En este blog encontrarás información sobre tecnologías como XNA, DirectX, Gaming y otras cositas relacionadas con el mundo del desarrollo de video juegos. Bienvenid@!

4 de julio de 2007

Crear una aplicación Direct3D con Visual C++ 2005 - Parte 3: Transformaciones

Continuando con la serie de posts sobre cómo crear una aplicación Direct3D con Visual C++ 2005 y habiendo aprendido a dibujar vértices transformados, es hora de abordar el dibujado de vértices propiamente en 3D para lo cual es necesario conocer y manejar el concepto de transformaciones, mediante las cuales se indica al sistema de Direct3D el lugar en el espacio con respecto a un origen en el que se encuentran los vértices, el punto y orientación desde el cual se están mirando y finalmente como se va a realizar la transformación de esa vista en 3D para proyectarla en una pantalla 2D.

La primera transformación, denominada Transformación de Mundo, nos permite Trasladar, Escalar y Rotar la geometría (los vértices) en el espacio 3D. Cada transformación es matemáticamente definida mediante una matriz de 16x16 especificada en C++ usando el tipo D3DXMATRIXA16. Para inicializar una matriz con los valores adecuados, la API de Direct3D provee una serie de funciones que nos ayudan con esta tarea. En nuestro ejemplo vamos a crear una matriz que represente una rotación de los vértices en torno al eje Y según el tiempo actual usando la función D3DXMatrixRotationY.

/* Declara la matriz para la transformacion de mundo */
D3DXMATRIXA16 matWorld;

/* Calcula el ángulo basado en el tiempo transcurrido para realizar un giro de 180° cada segundo */
UINT iTime = timeGetTime() % 1000;
FLOAT fAngle = iTime * (2.0f * D3DX_PI) / 1000.0f;

/* Inicializa la matriz con una transformacion de rotacion alrededor del eje Y */
D3DXMatrixRotationY (&matWorld, fAngle);

/* Fija la transformación de mundo para el dispositivo */
m_pD3DDevice->SetTransform (D3DTS_WORLD, &matWorld);

Otras funciones para trabajar con la matriz de transformación de mundo son: D3DXMatrixRotationY, D3DXMatrixRotationAxis, D3DXMatrixTranslation, D3DXMatrixScaling, entre otras.

La siguiente transformación, la Transformación de Vista, define la posición y rotación de la vista. Esta transformación puede ser vista como la cámara de la escena y generalmente es calculada a partir de 3 vectores: el punto de vista, el punto a mirar, y la dirección “arriba”. Cómo su nombre lo indica el punto de vista es la ubicación de la cámara o del ojo que está mirando la escena, el punto a mirar representa el punto hacia el cual está apuntando la cámara desde su ubicación, y finalmente la dirección arriba es un vector que define cómo esta rotada la cámara, por ejemplo, la escena puede estar siendo observada con la cámara boca abajo o girada hacia un costado en un ángulo dado.

El siguiente fragmento de código define una matriz de Transformación de Vista usando como punto de vista la posición (0.0, 3.0, -5.0) del espacio (X, Y, Z) respectivamente, como punto a mirar la posición (0.0, 0.0, 0.0) y como vector arriba el vector (0.0, 1.0, 0.0) que define el eje Y positivo como el lado arriba de la cámara.

/* Vector punto de vista */
D3DXVECTOR3 vEyePt (0.0f, 3.0f, -5.0f);

/* Vector punto a mirar */
D3DXVECTOR3 vLookatPt (0.0f, 0.0f, 0.0f);

/* Vector lado arriba */
D3DXVECTOR3 vUpVec (0.0f, 1.0f, 0.0f);

/* Declara la matriz de vista */
D3DXMATRIXA16 matView;
D3DXMatrixLookAtLH (&matView, &vEyePt, &vLookatPt, &vUpVec);

/* Fija la transformacion de vista para el dispositivo */
m_pD3DDevice->SetTransform (D3DTS_VIEW, &matView);

Cuando dibujamos una escena 3D en DirectX, en realidad lo que hacemos es una proyección del espacio de vista 3D a un espacio o puerto de vista 2D, en ultimas, nuestra pantalla del PC. Para definir cómo se debe hacer esa proyección usamos la Transformación de Proyección. Típicamente la matriz para aplicar esta transformación se calcula usando la función D3DXMatrixPerspectiveFovLH como se muestra en siguiente ejemplo.

/* Declara la matriz para la transformación de proyección */
D3DXMATRIX matProj;

/* Inicializa la matriz con un campo de vista de PI/4, relacion de aspecto de 1.0, plano cercano de 1.0 y plano lejano de 100.0 */
D3DXMatrixPerspectiveFovLH (&matProj, D3DX_PI/4, 1.0f, 1.0f, 100.0f);

/* Fija la transformación de proyección para el dispositivo */
m_pD3DDevice->SetTransform (D3DTS_PROJECTION, &matProj);

Los parámetros pasados a la función D3DXMatrixPerspectiveFovLH son: la referencia a la matriz a inicializar, el campo de vista, la relación de aspecto y los planos cercano y lejano respectivamente. El campo de vista es el ángulo de visualización de la escena en el eje Y (o vector arriba) definido en la transformación de vista. La relación de aspecto es definida como el campo de vista divido entre la altura (típicamente 1.0). El plano cercano es la distancia mínima necesaria a la que un objeto se debe encontrar de la cámara para poder ser “dibujado” y el plano lejano es la distancia máxima en que los objetos se dibujan en la escena.

Este es el proceso necesario para fijar las transformaciones de Mundo, Vista y Proyección y en nuestro ejemplo este código se encuentra definido en la función SetupMatrices de la clase MyD3DApplication y es llamada dentro del método Render, luego de la llamada a BeginEscene. Cuando se hace la llamada a DrawPrimitive, el dispositivo usa la información fijada para las transformaciones y las aplica a las primitivas dibujadas para calcular el resultado final de la operación y guardarlo en el buffer actual (* Las operaciones DrawPrimitive no se dibujan inmediatamente en la pantalla sino en un buffer trasero y es cuando se llama al método Present que se toma la información del siguiente buffer trasero y se muestra en el dispositivo de visualización).

Adicionalmente para que nuestro ejemplo funcione correctamente vamos a cambiar la definición de nuestro CUSTOMVERTEX y D3DFVF_CUSTOMVERTEX como se muestra a continuación usando la posición no transformada y el color.

/* Estructura para almacenar la informacion de los vértices */
struct CUSTOMVERTEX
{
  FLOAT x, y, z; // Posición 3D NO transformada para el vértice
  DWORD color; // Color del vértice
};

/* Formato de Vertice Flexible para CUSTOMVERTEX */
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE)

Además vamos a cambiar los valores usados para los vértices del triángulo a los siguientes con el fin de que se ubiquen dentro del campo de vista fijado en las transformaciones.

/* Define los vertices para el triangulo */
CUSTOMVERTEX vertices[] =
{
  { -1.0f, -1.0f, 0.0f, 0xffff0000, },
  { 1.0f, -1.0f, 0.0f, 0xff0000ff, },
  { 0.0f, 1.0f, 0.0f, 0xffffffff, },
};

Al ejecutar el código para este ejemplo (que puedes descargar desde aquí) podemos ver el siguiente triángulo girando en el espacio 3D.


Este triángulo se está dibujando con coordenadas no transformadas, lo que da más realismo y sensación de 3D al resultado final, sin embargo el color producido por cada vértice es el mismo sin importar su ubicación, esto es porque se está usando el color propio de cada vértice y no se está usando la característica de luces de Direct3D. En una próxima entrada trataremos esta interesante característica que ayuda a realzar el realismo de la escena 3D.

Código del ejemplo: Descargar

Hasta la próxima.

Willy R.

11 de junio de 2007

Crear una aplicación Direct3D con Visual C++ 2005 (Parte 2)

En el post anterior vimos como inicializar una ventana para una aplicación usando la API de Windows y luego como adquirir los objetos IDirect3D9 e IDirect3DDevice9, los cuales son necesarios para dibujar escenas en 3D usando DirectX 9. Sin embargo, no vimos como usar dichos objetos para mostrar nuestra escena en la pantalla.

En esta ocasión quiero mostrar algunos de los conceptos básicos usados para trabajar en 3D y realizar un demo simple para dibujar vértices en la pantalla.

En mi concepto, la finalidad de una aplicación Direct3D es mostrar en una pantalla plana (2D) “la imagen de cómo se vería una escena en 3 dimensiones vista desde cierto punto y bajo ciertas condiciones de luz y otras”. Para lograr esto las aplicaciones Direct3D usan muchos vértices, los cuales son “puntos” de un objeto que al agruparse, generalmente formando triángulos, pueden dar forma a un objeto en 3 dimensiones.

Para hacerlo práctico y por cuestiones de simplicidad vamos a comenzar por dibujar un triángulo usando vértices transformados. Un vértice transformado en realidad está en coordenadas 2D de la ventana, lo cual quiere decir que el punto (0,0) está en la esquina superior izquierda de la ventana, el eje X positivo esta a la derecha y el eje Y positivo es hacia abajo. Este tipo de vértices no usan luces de Direct3D porque ellos proporcionan su propio color.

Lo primero que hacemos es definir un tipo de estructura en el que podamos almacenar los datos de nuestros vértices (El código de este ejemplo usa la información del post anterior, aunque dicha funcionalidad se encapsulo en las clases WinApplication y D3DApplication; el código de la estructura CUSTOMVERTEX se encuentra en el archivo MyD3DApplication.h donde se define la clase para este ejemplo).

/* Estructura para almacenar la informacion de los vértices */
struct CUSTOMVERTEX
{
  FLOAT x, y, z, rhw; // Posición transformada para el vértice
  DWORD color; // Color del vértice
};

Posteriormente debemos definir el FVF (Flexible Vertex Format) para nuestro tipo CUSTOMVERTEX, esto es un valor que indique al motor de Direct3D que información vamos a pasar para cada vértice en las operaciones de dibujado.

/* Formato de Vertice Flexible para CUSTOMVERTEX */
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZRHW | D3DFVF_DIFFUSE)

Aquí D3DFVF_XYZRHW indica un punto transformado y D3DFVF_DIFFUSE indica el color del vértice.

Una vez que hemos definido el FVF podemos inicializar los valores para los tres (3) vértices del triángulo que queremos dibujar (Esto se hace en el método InitDirect3D en el archivo MyD3DApplication.cpp).

// Define los vertices para el triangulo
CUSTOMVERTEX vertices[] =
{
  { 320.0f, 50.0f, 0.5f, 1.0f, 0xffff0000, }, // x, y, z, rhw, color
  { 490.0f, 350.0f, 0.5f, 1.0f, 0xff00ff00, },
  { 150.0f, 350.0f, 0.5f, 1.0f, 0xffff00ff, },
};

Esto define tres (3) vértices para nuestro triangulo en las posiciones de pantalla (320, 50), (490, 350) y (150, 350) y de colores rojo, verde y rojo-azul respectivamente. Luego de esto debemos llamar al método IDirect3DDevice9::CreateVertexBuffer para crear el buffer de vértices en el dispositivo.

// Crea el buffer de vértices
if (FAILED (m_pD3DDevice->CreateVertexBuffer (3*sizeof (CUSTOMVERTEX),
  0 /*Usage*/, D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &m_pVertexBuffer, NULL)))
  return 0;

Los parámetros de CreateVertexBuffer indican el tamaño del buffer, el uso (0 o una combinación de constantes D3DUSAGE), el formato de los vértices, la clase de memoria que va a almacenar el buffer y la dirección del puntero al buffer (LPDIRECT3DVERTEXBUFFER9, equivalente a IDirect3DVertexBuffer9 *). El último parámetro es reservado y debe fijarse a NULL. De aquí cabe destacar que D3DPOOL_DEFAULT indica que el buffer se alojará en la memoria más conveniente, generalmente memoria de video.

Después de crear el buffer, es necesario copiar en él los datos de los vértices. Para hacer esto hay que bloquear el buffer usando el método IDirect3DVertexBuffer9::Lock. Si se puede bloquear, éste método fija en un puntero la dirección de memoria del inicio del buffer, con lo cual se pueden copiar los datos y posteriormente desbloquearlo.


VOID* pVertices;

// Bloquea el buffer y obtiene un puntero a su memoria
if (FAILED (m_pVertexBuffer->Lock (0, sizeof (vertices), (void**)&pVertices, 0)))
  return E_FAIL;

// Copia los datos de los vértices a la memoria del buffer
memcpy (pVertices, vertices, sizeof (vertices));

// Desbloquea el buffer
m_pVertexBuffer->Unlock ();

Finalmente es hora de renderizar lo hecho. Para esto vamos al método MyD3DApplication::Render en donde se llaman las operaciones IDirect3DDevice9:: BeginScene y IDirect3DDevice9::EndScene entre las cuales se deben realizar todas las operaciones de dibujado. Lo primero es fijar la fuente de datos para el dispositivo llamando al método IDirect3DDevice9::SetStreamSource pasando el puntero a nuestro buffer de vértices.

/* Fijar la fuente para la operacion de dibujado */
m_pD3DDevice->SetStreamSource (0, m_pVertexBuffer, 0, sizeof (CUSTOMVERTEX));

Luego fijamos el formato de vértice flexible que usa el buffer y llamamos al método IDirect3DDevice9::DrawPrimitive para hacer que el dispositivo “dibuje” el triangulo pasándole el valor constante del tipo de primitiva D3DPT_TRIANGLELIST, el cual indica al dispositivo que se va a dibujar una lista de triángulos desde el buffer (Aunque en nuestro caso es solo un triángulo como se indica en el último parámetro).

/* Fijar el formato de vértices de la fuente */
m_pD3DDevice->SetFVF (D3DFVF_CUSTOMVERTEX);

/* Dibuja el triangulo desde el buffer */
m_pD3DDevice->DrawPrimitive (D3DPT_TRIANGLELIST, 0, 1);

Con esto terminamos los pasos necesarios para dibujar el triangulo en la pantalla como vemos en la siguiente imagen donde se ve que cada vértice representa una esquina y que cada una tiene un color diferente que se mezcla suavemente en el centro con el color de los demás vértices.



Este ejemplo ya hace uso del objeto IDirect3DDevice9 para dibujar objetos en la pantalla aunque en un modo transformado. En un siguiente post veremos cómo usar vértices no transformados y usar transformaciones para modificar la escena 3D.

El código de este ejemplo para Visual Studio 2005 lo puedes descargar desde aquí

Willy R.

3 de mayo de 2007

Crear una aplicación Direct3D con Visual C++ 2005

En esta ocasión quiero mostrar un ejemplo de cómo usar la API (Application Programming Interface) de DirectX y la API de Windows para crear una aplicación Direct3D. Para esto necesitamos tener el siguiente software instalado:

Lo primero que hacemos es crear un proyecto de Aplicación de Consola de Win32 en lenguaje Visual C++ (o también puedes seleccionar proyecto de Aplicación de Windows Forms), como muestra la siguiente imagen.


Luego de esto nos muestra un asistente de configuración de la aplicación en el que seleccionamos las opciones Aplicación de Windows y Proyecto Vacio (es importante aquí seleccionar Aplicación de Windows ya que esto configura algunas directivas en el compilador para que nuestro código funcione).


Con esto hemos creado un proyecto vacio, así que lo siguiente que hacemos es agregar un archivo llamado main.cpp y en él vamos a incluir la librería y el método WinMain (que es el punto de entrada de la aplicación de Windows).

#include <windows.h>

int WINAPI WinMain (HINSTANCE hInst, HINSTANCE hPrevInstance, LPSTR lpCmdLine, INT nCmdShow)
{
  return 0;
}

Para poder mostrar una ventana debemos primero definir y registrar la clase de Windows que va a usar.

/* Inicializar la clase a registrar */
WNDCLASSEX wc =
{
  sizeof(WNDCLASSEX), // Tamaño de la estructura
  CS_CLASSDC, // Class style
  MsgProc, // Procedimiento de ventana
  0L, // Bytes extra. Fijar a 0L
  0L, // Bytes extra. Fijar a 0L
  hInst, // Instancia que contiene el proc. ventana para la clase
  NULL, // Icono
  NULL, // Cursor
  NULL, // Background brush
  NULL, // Menu class
  L"MyDirect3DApp", // Class name
  NULL // Small icon
};

/* Si falla el registro de la clase termina el programa */
if (!::RegisterClassEx (&wc))
  return 0;

El procedimiento de ventana (MsgProc) es el procedimiento encargado de manejar los mensajes que Windows pasa a la ventana cada que un evento ocurre. En nuestro caso es la siguiente función.

LRESULT WINAPI MsgProc (HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg)
  {
    case WM_DESTROY:
      Cleanup ();
      ::PostQuitMessage (0);
      return 0;

    case WM_PAINT:
      Render ();
      ::ValidateRect (hWnd, NULL);
      return 0;
  }

  return ::DefWindowProc (hWnd, msg, wParam, lParam);
}

Luego de registrar la clase de Windows debemos crear y mostrar la ventana donde Direct3D va a realizar el Render.

/* Crear la ventana de la aplicacion. */
g_hWnd = ::CreateWindow (
  L"MyDirect3DApp",
  L"Aplicacion Direct3D",
  WS_OVERLAPPEDWINDOW,
  0,
  0,
  800,
  600,
  ::GetDesktopWindow (),
  NULL,
  wc.hInstance,
  NULL);

/* Mostrar la ventana */
::ShowWindow (g_hWnd, nCmdShow);

La variable g_hWnd es de tipo HWND y es declarada por fuera del main para que sea global a todas las funciones. Con esto ya tenemos la ventana donde dibujar, pero para ello es necesario inicializar la interfaz Direct3D.

/* Creacion del objeto Direct3D */
g_pD3D = Direct3DCreate9 (D3D_SDK_VERSION);

Aquí g_pD3D también es declarada global y de tipo LPDIRECT3D9 el cual es un tipo de apuntador a una interfaz IDirect3D9. Esta interfaz nos permite entre otras cosas, verificar las capacidades del dispositivo de video e inicializar un dispositivo Direct3D para dibujar en una ventana.

Para que nuestro proyecto reconozca el tipo LPDIRECT3D9 y la función Direct3DCreate9 debemos incluir la librería <d3d9.h>, pero también debemos decirle a Visual Studio donde buscar las librerías de DirectX. Para hacerlo vamos a las propiedades del proyecto y en la opción Propiedades de Configuración / C++, adicionamos el directorio Include del directorio de instalación del SDK de DirectX a los Directorios de Include Adicionales, como lo muestra la siguiente figura.


Luego en la opción Linker adicionamos el directorio Lib/x86 a los Directorios de Library Adicionales así:


Finalmente en la opción Input de Linker agregamos d3d9.lib en las Dependencias Adicionales


Al inicializar el dispositivo es necesario indicar al motor de Direct3D los parámetros de presentación para nuestra aplicación tal como la ventana donde se va a dibujar, si se quiere mostrar en pantalla completa o en modo ventana y el formato que se va a utilizar y esto se hace mediante una variable del tipo estructura D3DPRESENT_PARAMETERS.

/* Parámetros de inicializacion */
D3DPRESENT_PARAMETERS pD3DParameters;

/* Parametros por defecto para crear el dispositivo */
ZeroMemory (&pD3DParameters, sizeof (pD3DParameters));

pD3DParameters.hDeviceWindow = g_hWnd;
pD3DParameters.EnableAutoDepthStencil = TRUE;
pD3DParameters.AutoDepthStencilFormat = D3DFMT_D16;
pD3DParameters.SwapEffect = D3DSWAPEFFECT_DISCARD;

/* Cambie a true para pantalla completa */
bool bFullScreen = false;

/* Si el modo es pantalla completa */
if (bFullScreen)
{
  /* En fullscreen se debe indicar el tamaño y formato de los back buffers */
  pD3DParameters.BackBufferWidth = 800;
  pD3DParameters.BackBufferHeight = 600;
  pD3DParameters.BackBufferFormat = D3DFMT_R5G6B5;
  pD3DParameters.BackBufferCount = 1;
  pD3DParameters.MultiSampleType = D3DMULTISAMPLE_NONE;
  pD3DParameters.Windowed = FALSE;
}
else
{
  pD3DParameters.Windowed = TRUE;
  pD3DParameters.BackBufferFormat = D3DFMT_UNKNOWN;
}

Con la estructura D3DPRESENT_PARAMETERS y usando el objeto Direct3D podemos inicializar el dispositivo Direct3D en la variable global g_pD3DDevice la cual es del tipo LPDIRECT3DDEVICE9 que representa un apuntador a una interfaz del tipo IDirect3DDevice9. Esta interfaz es la que luego nos va a permitir invocar una serie de operaciones de dibujado sobre el dispositivo gráfico.

/* Si no se puede crear el dispositivo retorna 0 */
if (FAILED (g_pD3D->CreateDevice (
  D3DADAPTER_DEFAULT,
  D3DDEVTYPE_HAL,
  g_hWnd,
  D3DCREATE_SOFTWARE_VERTEXPROCESSING,
  &pD3DParameters,
  &g_pD3DDevice ) ) )
  return 0;

Si se puede crear el dispositivo, la aplicación esta lista para iniciar la ejecución, esto es, verificar constantemente la cola de mensajes y procesarlos hasta que se cierre la ventana.

MSG msg;

/* Recibir el mensaje y manejarlo */
ZeroMemory (&msg, sizeof(msg));
while (msg.message != WM_QUIT)
{
  if (::PeekMessage (&msg, NULL, 0U, 0U, PM_REMOVE))
  {
    ::TranslateMessage( &msg );
    ::DispatchMessage( &msg );
  }
}

Como vemos en el procedimiento MsgProc, cuando llega el mensaje WM_PAINT se llama a la función Render, en la cual realmente se realiza el dibujado de los objetos en la pantalla. Para realizar este proceso se debe llamar a la función Clear del dispositivo con el fin de limpiar la pantalla (en este caso a un color azul), luego invocar la función BeginScene, la cual indica el comienzo del proceso de dibujado. Al terminar de dibujar todos los objetos de la escena, es necesario llamar a los métodos EndScene y Present para que se copie toda la información desde el buffer trasero al dispositivo y realmente se pinte en pantalla nuestro contenido.

VOID Render ()
{
  /* Limpia el buffer secundario */
  g_pD3DDevice->Clear (0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,255), 1.0f, 0L);

  /* Inicia el rendering */
  g_pD3DDevice->BeginScene ();

  /* Aquí van las operaciones de dibujado */

  /* Finaliza el dibujado */
  g_pD3DDevice->EndScene ();
  g_pD3DDevice->Present (NULL, NULL, NULL, NULL);
}

En el procedimiento de mensajes también se controla el evento WM_DESTROY que es lanzado luego de cerrar nuestra ventana. Cuando llega este mensaje se llama a la función Cleanup, en donde se libera el dispositivo Direct3D, así como también el objeto Direct3D.

VOID Cleanup ()
{
  if (g_pD3DDevice != NULL)
    g_pD3DDevice->Release ();

  if (g_pD3D != NULL)
    g_pD3D->Release ();
}

Adicionalmente se debe des-registrar la clase de Windows cuando el programa salga del bucle de mensajes.

::UnregisterClass (L"MyDirect3DApp", wc.hInstance);

Con esto hemos creado una aplicación que inicializa el dispositivo Direct3D y hace que se pinte de azul la ventana como se observa en la siguiente figura.


A primera vista esto no debería llevar tanto trabajo, es más, si hubiéramos desarrollado una aplicación de Windows Forms únicamente hubiéramos tenido que fijar la propiedad BackColor al color Blue, pero la diferencia radica en que aquí llevamos a cabo los pasos iniciales necesarios para poder usar el dispositivo Direct3D en el “dibujado” de objetos en 3 dimensiones. Sin embargo esto lo mostraré en un siguiente post, por ahora si tienes dudas sobre como usar la API de Windows para crear aplicaciones puedes visitar http://winapi.conclase.net/ en donde explican de una forma sencilla este proceso.

El código de este ejemplo para Visual Studio 2005 lo puedes descargar desde aquí.

Willy R.