lunes, 21 de abril de 2008

OpenGL: Introducción e inicialización

h0aX [hoax@fresnomail.com]

Nuevo año para BlackHat, nuevas entregas para sus seguidores. Este año me surge el capricho de presentarme más por la sección de Artículos (según el tiempo y el trabajo me lo permitan) y por su puesto, qué mejor manera de enriquecer nuestra querida sección de Códigos sino escribiendo motivadores artículos. Hace unos días encontraba me intercambiando con un amigo y miembro de la comunidad, ¿qué tipo de artículos podrían resultar interesantes para los lectores de BlackHat? y mientras pensaba yo en sniffers, IA y Prolog acomete mi amigo con una brillante idea: h0aX, escribe de OpenGL! Pues que así sea. Durante las próximas ediciones trataré en mayor o menor medida satisfacer la curiosidad de algunos que durante el tiempo que han permanecido tras las letras verdes y el fondo negro de nuestra revista, se han preguntado en alguna ocasión ¿qué es OpenGL?, ¿cómo puedo usarlo?

Sabido es por los seguidores de antaño que durante el pasado año he publicado algunos códigos del tema en cuestión, pues que sirvan estos de complemento para mis próximos artículos y les sacaremos mejor provecho. También quiero aclarar que mi mayor referencia para este trabajo consta fundamentalmente de dos documentos:
-El Libro Rojo de OpenGL (OpenGL RedBook)
-El Libro Azul de OpenGL (OpenGL BlueBook)
Así que si quedaran insatisfechos con la documentación que este trabajo representa ya saben a donde remitirse... y la verdad, espero que los realmente interesados queden insatisfechos :)

[(Como diría mi amigo DarkX: Zero is begin) ¿Qué es OpenGL?]

OpenGL (Open Graphics Library) no es más que una API que nos permitirá a nosotros, los programadores, sacar el mejor partido a nuestros dispositivos de gráficos y aceleradoras 3D, de modo que OpenGL es justo lo que estabas buscando si tu objetivo es la programación de gráficos avanzados en 2D/3D siendo amplia la gama de aplicaciones que bien pueden ir desde sistemas de simulaciones con alto valor científico hasta el más común de los juegos que hayas visto en cualquier consola. Esta API nos provee de una buena cantidad de comandos que nos permitirán realizar todas las tareas que puedas imaginar necesarias para el desarrollo de este tipo de aplicaciones, inicialización, definición de primitivas geométricas, definición de objetos complejos, aplicación de texturas, iluminación, transformación, etc. Bueno es aclarar que OpenGL (distinto a DirectX) solo nos facilitará el trabajo con gráficos, dejando a nuestro entero cuidado otras cuestiones como audio, red, o controles. Aún así esto no representa gran motivo de preocupación, y aunque resulta un poco anticipado solo diré que existen otras librerías similares como es el caso de OpenAL (Open Audio Library), así que por ahora solo tenlo en cuenta.

[Librerías acompañantes de OpenGL]

OpenGL como libraría independiente nos ofrece todas las funcionalidades necesarias para el trabajo con gráficos, no obstante surgen tareas realmente tediosas como es el caso de la proyección de matrices, posicionamiento de la cámara y otras para las cuales se han ideado librerías adicionales diseñadas expresamente para hacer más fácil nuestro trabajo.

GLU: Nos permite el trabajo no solo con primitivas geométricas, sino que nos ayuda a crear objetos un poco más complejos como cilindros, esferas, discos, y otros sin tener nosotros que preocuparnos si quiera de definir normales o coordenadas de texturas en caso que el objeto se defina texturizado o bajo la influencia de alguna luz en la escena. Nos facilita bastante también el trabajo con matrices. Esta librería la usaremos a menudo en nuestros proyectos. Glu siempre acompaña a las distribuciones de OpenGL.

GLUT: Independiente a la plataforma en que se está trabajando. Nos permite el trabajo de inicialización de ventanas, entradas del teclado y del ratón etc. Todo esto con independencia de si el código está siendo compilado en Windows, en Linux o en MacOS (¿Que no dije anteriormente que OpenGL se encuentra en múltiples plataformas? :o ) Además nos ofrece comandos para dibujar objetos más complejos. ¿Te acuerdas de la famosa cafetera que sale hasta en la sopa en todos los programas de diseño 3D? Quien ha visto el Teapot del 3DMax sabe a qué me refiero.

GLAUX: Muy parecida a Glut pero solo existe para Windows.

GLX: Librería para X-Windows en Linux, no diré mucho más de esta porque nunca la he usado.

OpenAL: La ya mencionada Open Audio Library. Nos permite el control del sonido, incluye soporte para sonidos en 3D, tan importante en aplicaciones de este tipo. Pues se vería bien mal que disparas contra tu objetivo con una Dragunov desde la cuarta planta de un edificio y escucharas el impacto del proyectil a tu lado, o que sintieras la deflagración de tus explosivos justo en frente cuando realmente han estallado a tu espalda.

OpenXX: Existen otras librerías similares a OpenAL que por ahora se encuentran fuera del alcance de este documento, es el caso de OpenNL (Network), OpenIN (Input), etc.

[Definiendo el entorno de trabajo]

Tal como les comentaba hace un momento, OpenGL es una librería multiplataforma, así que podrás encontrar distribuciones tanto en Windows, como en Linux o MacOS, incluso la última consola de la Nintendo, la admirable Nintendo Wii, utiliza OpenGL para generar sus gráficos según tengo entendido. Al momento de definir un entorno de trabajo he decidido tomar Windows XP como sistema operativo, C++ como lenguaje de programación y para ser más específico Borland C++ Builder 6 como compilador. Aunque estas son mis preferencias particulares, bien podrías utilizar otros compiladores o lenguajes. Para los programadores de Delphi les recomiendo Borland Delphi 6 en sustitución de Delphi 7 para estos menesteres.

[Prerrequisitos para comenzar a utilizar OpenGL]

Basta ya de tanta plática y vamos a los códigos. Primeramente, ¿qué necesitamos para comenzar a trabajar con OpenGL?

Para empezar estaría bien hacernos de una ventana donde dibujar nuestros objetos 3D, a menos que tengas en mente renderizar tus escenas en hologramas o algún otro dispositivo no convencional tendrás que conformarte con utilizar el DC de nuestras ventanas de Windows como lienzo portador de nuestro arte.

Comenzaremos en el menú File, New, Other... luego seleccionamos Console Wizard y marcamos las opciones tal como aparece en la imagen 2.



A partir de aquí asumo que posees conocimientos al menos básicos de cómo crear una ventana en Windows, que entiendes el bucle de tratamiento de mensajes, etc. De no ser así tendrás que estudiar un poco más antes de recibir esta documentación, luego pásate por el apéndice de este capítulo. Sustituye el código por este:

#include <windows.h>
#pragma hdrstop
#pragma argsused

HWND hWndMain; //almacena el handle de nuestra ventana
bool bFinished; //determina cuándo la aplicación ha sido terminada

/*Aquí escribiremos un poco de inicialización*/
void Start()
{
  bFinished = false;
}

/*Aquí liberaremos algunos recursos reservados*/
void Destroy()
{
  bFinished = true;
}

/*Una de las funciones más importantes, justo aquí dibujaremos*/
void Draw()
{
}

/*Controlaremos en esta función algunas cuestiones al momento de redefinir el tamaño de la ventana */
void Resize()
{
}

/*Bucle de control de mensajes de la ventana*/
LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch (uMsg)
  {
    case WM_SIZE:
    {
      Resize();
      return 0;
    }
    case WM_DESTROY:
    {
      Destroy();
      return 0;
    }
  }
  return DefWindowProcA(hwnd,uMsg,wParam,lParam);
}

WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  MSG msg;
  WNDCLASS wndclass;
  char szClassName[] = "OpenGL";

  hInst = hInstance;

//Llenamos la estructura wndclass
  wndclass.style = CS_VREDRAW | CS_HREDRAW;
  wndclass.lpfnWndProc = WndProc;
  wndclass.cbClsExtra = 0;
  wndclass.cbWndExtra = 0;
  wndclass.hInstance = hInstance;
  wndclass.hIcon = LoadIconA(NULL, IDI_APPLICATION);
  wndclass.hCursor = LoadCursorA(NULL, IDC_ARROW);
  wndclass.hbrBackground = GetSysColorBrush(COLOR_BTNFACE);
  wndclass.lpszMenuName = NULL;
  wndclass.lpszClassName = szClassName;

//registramos la clase
  RegisterClassA(&wndclass);

//creamos una ventana basados en esa clase
  hWndMain = CreateWindow(szClassName, szClassName, WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0, 0, 640, 480, NULL, NULL, hInstance, 0);
  ShowWindow(hWndMain, nCmdShow);
  Start();

//mientras la aplicación no esté finalizada
  while (!bFinished)
  {

//recegemos el mensaje de la cola y lo enviamos
//al bucle de tratamiento
    if ( PeekMessageA(&msg, 0,0,0, PM_REMOVE) )
    {
      TranslateMessage(&msg);
      DispatchMessageA(&msg);
    }
    else
    {
//Dibujaremos solo cuando la ventana no esté
//procesando mensaje alguno
      Draw();
    }
  }

  return msg.wParam;
}

Presiona F9 para correr la aplicación, si todo sale como es debido tendrás una ventana en pantalla completamente funcional que podrás redimensionar, mover, etc.

[Inicialización]

El proceso de inicialización de OpenGL puede resultar realmente engorroso y tedioso, pero normalmente los códigos que mostraré a continuación serán siempre los mismo para todos tus programas 3D, de modo que sobra la necesidad de memorizarlos, solo entenderlos bastará, además, en esta o en la segunda entrega me encargaré de encapsular toda la inicialización en una librería para hacer el trabajo mucho más simple.

Comenzaremos primeramente por agregar los includes necesarios.

#include <windows.h> //necesario para crear la ventana
#include <types.hpp> //utilizaremos alguna que otra estructura definida aquí
#include <gl\gl.h> //la librería principal de OpenGL
#include <gl\glu.h> //ya debes conocer GLU

Continuamos con las variables globales, verás que han sido agregadas dc y rc, estás variables serán imprescindibles para el renderizado y representan el device context y render context respectivamente.

HWND hWndMain; //almacena el handle de nuestra ventana
bool bFinished; //determina cuándo la aplicación ha sido terminada
HDC dc; //contexto de dispositivo
HGLRC rc; //contexto de render de OpenGL

Inicializamos...

Primeramente obtenemos el Device Context de nuestra ventana y lo almacenamos en dc. Luego comenzaremos a definir el formato de pixel que queremos, para ello usamos el descriptor pfd con doble buffer, colores RGBA y 32 bits de colores. Buscamos un formato correspondiente a la descripción que hemos hecho y lo seleccionamos. Posteriormente creamos y activamos el Render Context. Es importante en este punto redimensionar la ventana, en este caso lo he hecho a 640x480. Por último definimos algunos detalles como el color de fondo, el modelo de sombreado, activación del buffer de profundidad, etc.

Me corresponde hacer una aclaración importante respecto a la función glClearColor y más específicamente a la manera en que OpenGL utiliza los colores. Tal como conoces, todos los colores pueden ser representados a partir de los tres primarios, rojo, verde y azul, igualmente en OpenGL cuando defines un color tienes que hacerlo de la misma manera. A la función glClearColor le hemos pasado 0.2 como primer parámetro, 0.2 como segundo, 0.5 como tercero y 1.0 como cuarto o lo que es lo mismo 0.2 unidades de rojo, 0.2 de verde, 0.5 de azul y el cuarto parámetro representa la transparencia (RGBA = Red, Green, Blue, Alpha) con lo que hemos obtenido un azul opaco para el fondo de nuestro lienzo.

/* Aquí escribiremos un poco de inicialización*/
void Start()
{
  bFinished = false;
  PIXELFORMATDESCRIPTOR pfd;
  int pf;

//obtiene el contexto de dispositivo de la ventana
  dc = GetDC(hWndMain);

//describimos el formato de pixeles que queremos
  pfd.nSize = sizeof(pfd);
  pfd.nVersion = 1;
  pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER | 0;
  pfd.iPixelType = PFD_TYPE_RGBA;
  pfd.cColorBits = 32;

//vemos si está disponible y lo seleccionamos
  pf = ChoosePixelFormat(dc,&pfd);
  SetPixelFormat(dc,pf,&pfd);

//creamos el contexto de render y lo seleccionamos
  (void *)rc = wglCreateContext(dc);
  wglMakeCurrent(dc, rc);

//redimensionamos la ventana a 640x480
  SetWindowPos(hWndMain, NULL, 0, 0, 640, 480, 0);
  glClearColor(0.2,0.2,0.5,1.0);
//seleccionamos el color de fondo
  glShadeModel(GL_SMOOTH); //modelo de sombreado suavizado, normales por vértices
  glClearDepth(1.0); //define el valor con que se limpiará el buffer de profundidad
  glDepthFunc(GL_LESS); //valor usado en la comparación del buffer de profundidad
  glEnable(GL_DEPTH_TEST); //activamos buffer de profundidad
  glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST); //seleccionamos la corrección de perspectiva más rápida
  glEnable(GL_TEXTURE_2D); //activamos el texturado
  glEnable(GL_COLOR_MATERIAL); //activamos el color para los materiales
}

La función Destroy() para la liberación de recursos es bien sencilla. Simplemente desactivamos el Render Context de OpenGL y lo eliminamos al igual que al Device Context.

/*Aquí liberaremos algunos recursos reservados*/
void Destroy()
{
  wglMakeCurrent(0,0);
//desactivamos el render context
  wglDeleteContext(rc); //eliminanos el render context
  ReleaseDC(hWndMain, dc); //liberamos el device context de la ventana
  bFinished = true;
}

Aunque aun no dibujaremos nada en la ventana nos será necesario ir preparando la función Draw() para su uso. Por cada frame que dibujamos en pantalla necesitamos primeramente, borrar el contenido del lienzo, es decir, el buffer de colores y el de profundidad, cargar la Matrix Identidad (aun no corresponde hablar de matrices, eso lo veremos en próximos capítulos), forzamos la ejecución de los comandos GL con glFlush hasta ese momento y por último intercambiamos entre los buffers frontal y tracero con SwapBuffers. Para comprender correctamente la necesidad de SwapBuffers haremos una pausa para una aclaración. Si recuerdan cuando definimos el tipo de pixeles a utilizar escogimos un descriptor con doble buffer y justamente entre estos dos buffer la función SwapBuffers trabaja. Cuando la computadora tiene que representar una escena en movimiento debe hacerlo de la manera más rápida posible para que nuestro juego/simulador no corra lento, consideren entonces que a cada frame debe borrarse lo que hay dibujado en la pantalla y pintarlo de nuevo. Esta tarea no presenta gran problema en escenas sin animación pero cuando está presente la misma aparece un efecto indeseado de pestañeo. La manera de corregirlo es mostrando un lienzo(buffer), mientras el otro está siendo borrado y redibujado(back buffer), luego se intercambian.

/* Una de las funciones más importantes, justo aquí dibujaremos*/
void Draw()
{
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glLoadIdentity();

  glFlush();
  SwapBuffers(dc);
}

Nos corresponde en este punto definir el comportamiento de la ventana cuando esta es redimensionada. Primeramente obtenemos el ancho y el alto, si el alto se encuentra en 0 le asignamos 1 para evitar una división por 0 en las próximas líneas. Usaremos la función glViewport para especificar el tamaño del lienzo, los primeros dos parámetros hacen referencia a la esquina superior izquierda (normalmente serán cero los dos) los dos últimos a la esquina inferior derecha que corresponden justamente con el ancho y alto de la ventana respectivamente. Otra función importante en el proceso de redimensión es gluPerspective que define justamente la perspectiva de la cámara. gluPerspective funciona tal como si estuviéramos escogiendo el lente que usará nuestra cámara y a través del cual será visto el mundo. El primer parámetro indica la amplitud del lente, 65º en este caso. Este valor resulta útil a la hora de realizar ciertos efectos de cámara como el zoom de una mira telescópica o el efecto "ojo de pescado" de la mini cámara de un misil teledirigido. El segundo parámetro indica el radio de visión, el tercero la distancia a partir de la cual comienzan a dibujarse los objetos y el último parámetro la distancia a partir de la cual ya no se dibujarán los objetos. Con estos cuatro parámetros hemos definido lo que se conoce como el frustrum de la cámara.

/* Controlaremos en esta función algunas cuestiones al momento de redefinir el tamaño de la ventana */
void Resize()
{
  TRect WinRect;
  int Width, Height;
  GetWindowRect(hWndMain, &WinRect);

  Width = WinRect.Right - WinRect.Left;
  Height = WinRect.Bottom - WinRect.Top;

  if (Height == 0)
  {
    Height += 1;
    SetWindowPos(hWndMain, NULL, WinRect.Left, WinRect.Top, Width,Height,0);
  }

  glViewport(0, 0, Width, Height);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(65, (GLfloat)Width/(GLfloat)Height, 0.1f, 100.0f);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
}

El resto de nuestro código por ahora aparece invariable pues no hemos hecho cambio alguno en el bucle de tratamiento de mensajes o en la función WinMain.

/* Bucle de control de mensajes de la ventana*/
LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch (uMsg)
  {
    case WM_SIZE:
    {
      Resize();
      return 0;
    }

    case WM_DESTROY:
    {
      Destroy();
      return 0;
    }
  }

  return DefWindowProcA(hwnd,uMsg,wParam,lParam);
}

WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  MSG msg;
  WNDCLASS wndclass;
  char szClassName[] = "OpenGL";

  hInst = hInstance;

//Llenamos la estructura wndclass
  wndclass.style = CS_VREDRAW | CS_HREDRAW;
  wndclass.lpfnWndProc = WndProc;
  wndclass.cbClsExtra = 0;
  wndclass.cbWndExtra = 0;
  wndclass.hInstance = hInstance;
  wndclass.hIcon = LoadIconA(NULL, IDI_APPLICATION);
  wndclass.hCursor = LoadCursorA(NULL, IDC_ARROW);
  wndclass.hbrBackground = GetSysColorBrush(COLOR_BTNFACE);
  wndclass.lpszMenuName = NULL;
  wndclass.lpszClassName = szClassName;

//registramos la clase
  RegisterClassA(&wndclass);

//creamos una ventana basados en esa clase
  hWndMain = CreateWindow(szClassName, szClassName, WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0, 0, 640, 480, NULL, NULL, hInstance, 0);

  ShowWindow(hWndMain, nCmdShow);

  Start();

//mientras la aplicación no esté finalizada
  while (!bFinished)
  {

//recegemos el mensaje de la cola y lo enviamos
//al bucle de tratamiento
    if ( PeekMessageA(&msg, 0,0,0, PM_REMOVE) )
    {
      TranslateMessage(&msg);
      DispatchMessageA(&msg);
    }
    else
    {

//Dibujaremos solo cuando la ventana no esté
//procesando mensaje alguno
      Draw();
    }
  }

  return msg.wParam;
}

Corriendo la aplicación que acabamos de escribir debemos ver una ventana como esta. Por ahora solo tenemos un lienzo en blanco pero ya habremos superado el proceso de inicialización de OpenGL.

[Encapsulando la inicialización]

Resultaría sin duda alguna tediosa y poco alentadora la necesidad de escribir todo este volumen de código para solamente preparar la ventana donde dibujaremos por cada vez que iniciamos un proyecto en OpenGL. Con motivo de hacernos la vida más fácil y debido a que la inicialización comúnmente será la misma, convendría construirnos una librería que encapsulara la tarea de iniciar OpenGL y de este modo obtendremos un código mucho más funcional y limpio.

[La clase CGL]

//---------------------------------------------CGL.H---------------------------------------------
#include <glaux.h>
#include <types.hpp>
#include <windows.h>

class CGL
{
  private:
  HDC dc;
//Device Context
  HGLRC rc; //Render Context
  public:
  CGL();
  ~CGL();
  void InitOpenGL(HWND hWnd);
//Inicia OpenGL
  void Resize(HWND hWnd); //Control de redimensión de ventana
  void StopOpenGL(HWND hWnd); //Termina OpenGL
  void StartSceneDraw(); //Comienza el dibujado, todo lo que se dibuje debe encontrarse tras esta función
  void StopSceneDraw(); //Termina el dibujado
};

//---------------CGL.CPP-----------------
/********************************
**
** Nombre: CGL.cpp
**
** Autor: h0aX hoax@fresnomail.com
**
** Fecha: 30/03/2008
**
** Descripción: Tareas básicas de inicialización de OpenGL.
** Tutorial OpenGL para BlackHat. 0 Inicialización.
**
**
*********************************/
#include "CGL.h"

CGL::CGL()
{
}

CGL::~CGL()
{
}

void CGL::InitOpenGL(HWND hWnd)
{
  PIXELFORMATDESCRIPTOR pfd;
  int pf;

  dc = GetDC(hWnd);

  pfd.nSize=sizeof(pfd);
  pfd.nVersion=1;
  pfd.dwFlags=PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER | 0;
  pfd.iPixelType=PFD_TYPE_RGBA;
  pfd.cColorBits=32;

  pf = ChoosePixelFormat(dc,&pfd);
  SetPixelFormat(dc,pf,&pfd);

  (void *)rc = wglCreateContext(dc);
  wglMakeCurrent(dc, rc);

  SetWindowPos(hWnd, NULL, 0, 0, 640, 480, 0);

  glClearColor(0.2,0.2,0.5,1.0);
  glShadeModel(GL_SMOOTH);
  glClearDepth(1.0);
  glDepthFunc(GL_LESS);
  glEnable(GL_DEPTH_TEST);
  glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST);
  glEnable(GL_TEXTURE_2D);
  glEnable(GL_COLOR_MATERIAL);
}

void CGL::Resize(HWND hWnd)
{
  TRect WinRect;
  int Width, Height;
  GetWindowRect(hWnd, &WinRect);

  Width = WinRect.Right - WinRect.Left;
  Height = WinRect.Bottom - WinRect.Top;

  if (Height == 0)
  {
    Height+=1;
    SetWindowPos(hWnd, NULL, WinRect.Left, WinRect.Top, Width,Height,0);
  }

  glViewport(0, 0, Width, Height);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective( 65, (GLfloat)Width/(GLfloat)Height, 0.1f, 5000.0f);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

}

void CGL::StopOpenGL(HWND hWnd)
{
  wglMakeCurrent(0,0);
  wglDeleteContext(rc);
  ReleaseDC(hWnd, dc);
}

void CGL::StartSceneDraw()
{
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glLoadIdentity();
}

void CGL::StopSceneDraw()
{
  glFlush();
  SwapBuffers(dc);
}

[Implementación de la clase CGL]

Para concluir solo nos resta conocer la manera de implementar la clase CGL.

#include <windows.h>
#include <CGL.h>

#pragma hdrstop
#pragma argsused

//variables globales
HWND hWndMain;
bool bFinished;
CGL *gl;

//prototipos
void Start();
void Destroy();
void Draw();
LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

void Start()
{
  bFinished = false;
  gl = new CGL();
  gl->InitOpenGL(hWndMain);
//inicializamos en la función Start
}

void Destroy()
{
  gl->StopOpenGL(hWndMain);
//Liberación de recursos
  delete gl;
  bFinished = true;
}

void Draw()
{
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch (uMsg)
  {
    case WM_SIZE:
    {
      gl->Resize(hWndMain);
//Redimensión
      return 0;
    }

    case WM_DESTROY:
    {
      Destroy();
      return 0;
    }
  }

  return DefWindowProcA(hwnd,uMsg,wParam,lParam);
}

WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  MSG msg;
  WNDCLASS wndclass;
  char szClassName[] = "Tutorial OpenGL 0 de h0aX - Inicialización";

  wndclass.style = CS_VREDRAW | CS_HREDRAW;
  wndclass.lpfnWndProc = WndProc;
  wndclass.cbClsExtra = 0;
  wndclass.cbWndExtra = 0;
  wndclass.hInstance = hInstance;
  wndclass.hIcon = LoadIconA(NULL, IDI_APPLICATION);
  wndclass.hCursor = LoadCursorA(NULL, IDC_ARROW);
  wndclass.hbrBackground = GetSysColorBrush(COLOR_BTNFACE);
  wndclass.lpszMenuName = NULL;
  wndclass.lpszClassName = szClassName;

  RegisterClassA(&wndclass);

  hWndMain = CreateWindow(szClassName, szClassName, WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0, 0, 10, 10, NULL, NULL, hInstance, 0);

  ShowWindow(hWndMain, nCmdShow);

  Start();

  while (!bFinished)
  {
    if (PeekMessageA(&msg, 0,0,0, PM_REMOVE))
    {
      TranslateMessage(&msg);
      DispatchMessageA(&msg);
    }
    else
    {
      gl->StartSceneDraw();
//comienza el dibujado
      Draw(); //función de dibujado
      gl->StopSceneDraw(); //termina el dibujado
    }
  }

  return msg.wParam;
}

En la carpeta Tools de esta edición se incluye un ejemplo para mostrar lo aprendido hasta el momento de OpenGL.

[Apéndice para este capítulo]

Al concluir este primer capítulo imagino que habrán aprendido muchos algunas cosas nuevas sobre OpenGL, otros habrán aprendido incluso temas más básicas como la forma manual de hacernos de una ventana en Windows, pues seguramente estaban acostumbrados a desarrollar esta tarea de forma visual con el VCL de Borland. A modo de apéndice quisiera agregar algunas notas para mostrarles la manera de inicializar OpenGL sin necesidad de encargarnos nosotros de la creación de la ventana.

Normalmente cuando abrimos C++ Builder 6 inmediatamente el IDE nos muestra un nuevo proyecto creado, listo para ser usado por nosotros. Este proyecto incluye como elemento básico una ventana prefabricada por el VCL (Visual Component Library) en la cual podremos agregar controles todo de forma visual y sencilla. En este caso, y para facilitar el trabajo en OpenGL a las personas que solo saben utilizar el VCL, haremos uso de esta ventana para realizar con ella todas las tareas que vimos en temáticas anteriores.

Primeramente, en la unit correspondiente al Form1 agregaremos el header de nuestra librería CGL en el include.

#include <vcl.h>
#include <CGL.h>

Seguidamente declaramos una variable global correspondiente a nuestro objeto gl.

TForm1 *Form1;
CGL *gl;

Activamos el evento OnCreate del formulario y escrbimos la inicialización. OnCreate corresponde a la función Start() vista en los ejemplos anteriores.

void __fastcall TForm1::FormCreate(TObject *Sender)
{
  gl = new CGL();
  gl->InitOpenGL(Handle);
}

En el evento OnDestroy definimos la liberación de recursos, OnDestroy equivale a la función Destroy() antes vista.

void __fastcall TForm1::FormDestroy(TObject *Sender)
{
  gl->StopOpenGL(Handle);
  delete gl;
}

Ya debes imaginar el trabajo correspondiente al evento OnResize

void __fastcall TForm1::FormResize(TObject *Sender)
{
  gl->Resize(Handle);
}

Si tienes buena memoria recordarás que en los ejemplos anteriores escribimos el bucle de tratamiento de mensajes de tal manera que la función Draw() para dibujar solo era invocada mientras la ventana no tuviera mensajes por procesar. Si intentáramos hacerlo de otra forma la aplicación no nos mostrará los resultados deseados. El medio de hacer que nuestra aplicación ejecute determinado código cuando no tenga mensajes pendientes para procesar es la siguiente.

En la paleta Aditional de componentes busca uno llamado ApplicationEvets e insertalo al formulario.

Luego presiona dobleclick sobre el evento OnIdle de este componente y en su interior escribe el código para el dibujado.

void __fastcall TForm1::ApplicationEvents1Idle(TObject *Sender, bool &Done)
{
  gl->StartSceneDraw();
  gl->StopSceneDraw();
  Done = false;
}

De haber seguido todos los pasos indicados obtendrás los mismos resultados que en los primeros ejemplos sin el VCL. Algunos curiosos se estarán preguntando por qué me abstengo a usar el VCL en mis ejemplos. Pues comienza por comparar los tamaños entre el ejecutable compilado con el VCL y el que no, mientras uno se lleva 448 Kb el otro ocupa solo 53Kb.

Escribir tu mismo el código de la ventana significa que tendrás un poco más de control sobre la misma, es muy probable que si continuas estudiando OpenGL en el futuro prefieras escribir tú todo el código en lugar de dejarle la tarea al VCL de Borland. También conviene decir que de esta manera podrás escribir tus programas OpenGL en cualquier compilador de C/C++ logrando deshacerte de cierta dependencia del compilador.

Hasta aquí este primer capítulo introductorio a la programación de gráficos avanzados en OpenGL. Por el momento nuestro lienzo permanece mudo y la función Draw() casi vacía. En la próxima entrega veremos más y hablaremos de creación de primitivas geométricas.

Quisiera que las personas interesadas en este y en los siguientes artículos que conformarán el tutorial me expresen su interés a hoax@fresnomail.com de modo que pudiera saber a cuantos y a quienes están siendo dirigidos mis esfuerzos, además de poder personalizar un poco el contenido con lo que ustedes consideren más interesante. De las personas que escriban dependen las próximas entregas, pues no tiene sentido escribir un tutorial de 10 capitulos con 20 páginas cada uno que solo vaya dirigido a 3 o 4 personas.



Artículos relacionados


No hay comentarios: