Tu primer programa

<<< BAJATE EL CODIGO FUENTE AQUI >>>

Bueno, ya va siendo hora de que empezemos a hacer algun programa, mas que nada porque es la meta de estos tutoriales.

Este primer ejemplo es lo más sencillo que se puede hacer con Direct3D, aunque tras haberlo leido por completo ya sabras:

- Usar Matrices
- Usar Vertices
- Vertex Buffer

Voy a ir comentando las partes más importantes del programa y explicando que hace exactamente cada línea. Para tener una visión global de lo que es el programa y lo que hace, bajate el codigo fuente y lo ejecutas. Asi de paso compruebas que tienes bien configurado el Visual C++.

 

La clase principal

Esta es la clase principal de nuestro programa:

class CMyD3DApplication : public CD3DApplication
   {
   LPDIRECT3DVERTEXBUFFER8 m_pVB; // Buffer to hold vertices
   DWORD m_dwSizeofVertices;
 HRESULT ConfirmDevice( D3DCAPS8*, DWORD, D3DFORMAT );
protected:
   HRESULT OneTimeSceneInit();
   HRESULT InitDeviceObjects();
   HRESULT RestoreDeviceObjects();
   HRESULT InvalidateDeviceObjects();
   HRESULT DeleteDeviceObjects();
   HRESULT Render();
   HRESULT FrameMove();
   HRESULT FinalCleanup();
   public:
   CMyD3DApplication();
   };


Las dos primeras variables sirven para guardar los vertices que se usan para dibujar los triangulos. Estos vertices se almacenan en un Vertex Buffer, y como es algo bastante importante vamos a verlo mas adelante en un poco mas en detalle.

De momento continuamos con nuestro primer programa... tras definir las dos variables para los vertices, tenemos la funcion ConfirmDevice que sirve para comprobar las caracteristicas de nuestra tarjeta.

Por ultimo aparecen los demas metodos que definen los "eventos" de nuestro programa, que son estos eventos esta explicado en el capitulo anterior. (no hace falta que te diga vuelve para atras si no sabes lo que son los eventos, ya que es algo importante)

Vamos a ver que sucede en estas funciones, solamente en algunas sucede algo (en el resto se dejan vacias y se devuelve un S_OK)

RestoreDeviceObjects()

Se llama a este metedo despues de InitDeviceObjects() cuando comienza el programa. Tambien se llama cada vez que se redimensiona la ventana, aunque es llamado despues de InvalidateDeviceObjects().

HRESULT CMyD3DApplication::RestoreDeviceObjects()
{
  	// Set the transform matrices
  	D3DXVECTOR3 vEyePt = D3DXVECTOR3( 0.0f, 0.0f, 2.0f );
  	D3DXVECTOR3 vLookatPt = D3DXVECTOR3( 0.0f, 0.0f, 0.0f );
  	D3DXVECTOR3 vUpVec = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );
  	D3DXMATRIX matWorld, matView, matProj;
	D3DXMatrixIdentity( &matWorld );

  	D3DXMatrixLookAtLH( &matView, &vEyePt, &vLookatPt, &vUpVec );
  	FLOAT fAspect = m_d3dsdBackBuffer.Width / (FLOAT)m_d3dsdBackBuffer.Height;
  	D3DXMatrixPerspectiveFovLH( &matProj, D3DX_PI/4, fAspect, 1.0f, 500.0f );
   
  	m_pd3dDevice->SetTransform( D3DTS_WORLD, &matWorld );
  	m_pd3dDevice->SetTransform( D3DTS_VIEW, &matView );
  	m_pd3dDevice->SetTransform( D3DTS_PROJECTION, &matProj );
 	// fill the vertex buffer with the new data
   	D3DVIEWPORT8 vp;
   	m_pd3dDevice->GetViewport(&vp);
 	// Initialize to render a quad
   	CUSTOMVERTEX cvVertices[] =
   	{
   	{ 0.0f, 0.0f, 0.5f, 1.0f, 0xffff0000, }, // x, y, z, rhw, color
   	{ (float)vp.Width, 0.0f, 0.5f, 1.0f, 0xffff0000, },
  	{ (float)vp.Width, (float)vp.Height, 0.5f, 1.0f, 0xff00ff00, },
   	{ 0.0f, (float)vp.Height, 0.5f, 1.0f, 0xff00ffff, },
   	};

	m_dwSizeofVertices = sizeof(cvVertices);
	// Create the vertex buffer
  	if( FAILED( m_pd3dDevice->CreateVertexBuffer( m_dwSizeofVertices,
  	0, D3DFVF_CUSTOMVERTEX,
  	D3DPOOL_MANAGED, &m_pVB ) ) )
   		return E_FAIL;
	VOID* pVertices;
  	if( FAILED( m_pVB->Lock( 0, m_dwSizeofVertices, (BYTE**)&pVertices, 0    ) ) )
   	return E_FAIL;
   	memcpy( pVertices, cvVertices, m_dwSizeofVertices);
   	m_pVB->Unlock();
   
   	return S_OK;
}

En otras palabras :) lo que se hace aqui es inicializar las matrices del mundo, la vista y las transformaciones.

La matriz mundo se inicializa a la matriz identidad (ya explicare que es esto mas adelante, de momento vamos a hacer que todo funcione...) Para ello se usa D3DXMatrixIdentity().

La matriz Vista se inicializa con unos cuantos valores por defecto:

D3DXVECTOR3 vEyePt = D3DXVECTOR3( 0.0f, 0.0f, 2.0f );
D3DXVECTOR3 vLookatPt = D3DXVECTOR3( 0.0f, 0.0f, 0.0f );
D3DXVECTOR3 vUpVec = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );

vEyePt es el punto donde se coloca la camara o el ojo que va a ver la escena. vLookatPt es el puntoo hacia donde mira, y vUpVec es un vector que apunta en la direccion del eje Y por defecto, indica si la camara esta de lado o no, es decir, indica qué es "arriba" para la camara y que es "abajo". Imaginate un simulador de vuelo, cuando el avion ladea hacia un lado es como si girases la camara, y cuando va recto el cielo esta arriba.... pues esto es lo que se define con el vector Up.

Llamamos a D3DXMatrixLookAtLH() para inicializar la camara al sistema de coordenadas "left-handed" que es el que usa en Direct3D por defecto.

El "aspect ratio" de la perspectiva depende del tamaño del back buffer...

FLOAT fAspect = m_d3dsdBackBuffer.Width / (FLOAT)m_d3dsdBackBuffer.Height;
D3DXMatrixPerspectiveFovLH( &matProj, D3DX_PI/4, fAspect, 1.0f, 500.0f );

Una vez que tenemos definidas las matrices, las pasamos al pipeline de Direct3D (entendamoslo como aquello que transforma nuestras matrices en algo que se puede ver en la pantalla?) con la funcion SetTransform().

Antes de que te cages en mis muelas por hablar de tanta matriz sin haberlas explicado.... que lo explicare en el siguiente capitulo. de momento vamos a ver como se hace y compilar nuestro primer programilla.
Pero antes, veamos que es el vertex buffer porque si que es importante.

Vertex Buffer

Es un objeto que almacena arrays de vertices. Estos vertices se suelen almacenar en la memoria de la tarjeta de video, donde esta mucho mas accesible para hacer calculos 3D rapidamente. Es importante cuando necesitamos velocidad en las transformaciones y efectos de luz.

Como creamos un Vertex Buffer?

if( FAILED( m_pd3dDevice->CreateVertexBuffer( m_dwSizeofVertices, 0, D3DFVF_CUSTOMVERTEX, D3DPOOL_MANAGED, &m_pVB ) ) )
return E_FAIL;

- m_dwSizeofVertices es el tamaño de los vertices
- D3DFVF_CUSTOMVERTEX especifica como son nuestros vertices (se explica despues)
- m_pVB almacena el puntero al vertex buffer creado

La miga esta en D3DFVF_CUSTOMVERTEX , que le dice a Direct3D como son los vertices que vamos a usar. D3DFVF_CUSTOMVERTEX esta definido al principio del programa en

#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZRHW | D3DFVF_DIFFUSE)

basicamente este define lo que hace es dar otro nombre a los flags que deberiamos usar al crear el VertexBuffer, pero es mas sencillo hacer un define, no? Estos flags activan o desactivan que caracteristicas tienen nuestros vertices, es importante usar solo los flags necesarios porque ahorra memoria y en tiempo de calculo.

Que son estos flags?

- D3DFVF_XYZRHW indica que los vertices ya han sido transformados y no necesitan ser transformados de nuevo, si pasaramos los vertices sin transformar deberiamos usar el flag D3DFVF_XYZ
- D3DFVF_DIFFUSE indica que nuestros vertices incluyen un componente de color difuso (o alpha)

Para mas adelante que usemos mas flags, es importante saber que hay que definir los flags en este orden:

1. Posicion del vertice: float x, float y, float z
2. RJW (reciprocal of homogeneus w coordinate) solo para vertices ya transformados
3. Valores de mezclado de 1 al 5 (Blending)
4. Normal del vertice: float x, float y, float z
5. Tamaño de punto del vertice
6. Color difuso (RGBA)
7. Color Specular (RGBA)
8, Coordenada de la textura de 1 a 8 (coordenadas u y v)

Un ejemplo para ilustrar todo esto:

Si nuestra estructura de vertices es como esta:

typedef struct
{
D3DVALUE x,y,z;
D3DVALUE diffuse;
D3DVALUE u,v;
} MYVERTEX;

hay que definir unas flags como estas:

#define D3DFVF_CUSTOMVERTEX ( D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1 )

Otro ejemplo mas sencillo podria ser:

para esta estructura,,,

struct LineVertex
{
float x,y,z;
}


corresponderia esta definicion de flags...

#define LINE_VERTEX ( D3DFVF_XYZ )

Y para que no se diga, otro ejemplo mas!!! esta vez tope complicado para ver si lo has pillado:

typedef struct SObjVertex
{
D3DVALUE x,y,z;
D3DVALUE nx,ny,nz;
DWORD diffuse;
DWORD specular;
TextureCoords2 t0;
TextureCoords2 t0;
TextureCoords2 t0;
TextureCoords2 t0;
} SObjVertex;
...
DWORD gSObjVertexFVF = ( D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_SPECULAR | D3DFVF_NORMAL | D3DFVF_TEX4 |
D3DFVF_TEXCOORDSIZE2(0) | D3DFVF_TEXCOORDSIZE3(1) | D3DFVF_TEXCOORDSIZE2(2) | D3DF_TEXCOORDSIZE3(3));

Bueno, y ya vale de tanto ejemplo que creo esta suficientemente claro. Volviendo a como creamos nuestro vertex buffer ... vamos a ver que es el ultimo parametro que se le pasa a la funcion, tiene que ver con el manejo de recursos. Direct3D puede administrar automaticamente los recursos que se usan para mipmaps, volume maps, cube maps, index buffers, y como no para vertex buffers. Esto lo hacemos pasandole como parametro D3DPOOL_MANAGED a la funcion CreateVertexBuffer(). Esto define que tipo de memoria va a almacenar nuestros vertices. Te preguntaras porque se usan diferentes memorias... bien, puede que tengas 64 MB DDRAM en tu tarjeta grafica, y puede que tengas 128 MB RAM en tu equipo; toda esta memoria debe ser usada correctamente para cada tipo de tarea. La mejor manera de no equivocarse es dejar que Direct3D se encarge de ello... usa siempre D3DPOOL_MANAGED a no ser de que conozcas otra mejor. :-)

Una vez hemos creado el vertex buffer y tenemos ya el puntero (m_pVB) debemos rellenar este buffer con nuestros vertices:

VOID* pVertices;
if( FAILED( m_pVB->Lock( 0, m_dwSizeofVertices, (BYTE**)&pVertices, 0 ) ) )
return E_FAIL;
memcpy( pVertices, cvVertices, m_dwSizeofVertices);
m_pVB->Unlock();

para rellenar el buffer se hace un simple memcpy como puedes ver. Pero antes de eso hay que bloquear el buffer. En realidad solo se deberia bloquear partes del buffer. Para esto esta la funcion Lock...

HRESULT Lock(
UINT OffsetToLock,
UINT SizeToLock,
BYTE** ppbData,
DWORD Flags
);

Para bloquear segun que partes se usa OffsetToLock y SizeToLock. En nuestro caso vamos a bloquear todo el buffer, asi que desde 0 hasta m_dwSizeofVertices.

Echa una ojeada al codigo anterior mas detenidamente y veras que hay una variable que me he sacado de la manga, es cvVertices. Que es cvVertices? Es una estructura que contiene los vertices con los que queremos rellenar el VertexBuffer y que deberiamos haber rellanado antes de esta forma:

CUSTOMVERTEX cvVertices[] =
{
{ 0.0f, 0.0f, 0.5f, 1.0f, 0xffff0000, }, // x, y, z, rhw, color
{ (float)vp.Width, 0.0f, 0.5f, 1.0f, 0xffff0000, },
{ (float)vp.Width, (float)vp.Height, 0.5f, 1.0f, 0xff00ff00, },
{ 0.0f, (float)vp.Height, 0.5f, 1.0f, 0xff00ffff, },
};

El primer vertice esta en 0,0 (esquina sup izq). El segundo arriba a la derecha, el tercero abajo a la derecha, y el cuarto abajo a la izquierda. Recuerda, siempre en sentido de las agujas del reloj!!

 

Viewports

Para crear la estructura anterior hemos echado mano de vp.Width y vp.Height que nos dan el ancho y alto de nuestra ventana. vp hace referencia a nuestro ViewPort y se obtiene de la siguiente manera:

D3DVIEWPORT8 vp;
m_pd3dDevice->GetViewport(&vp);

Un viewport hace referencia a la parte de nuestra aplicacion que que se va a renderizar. La estructura D3DVIEWPORT8 contiene X, Y, Ancho y Alto:

typedef struct _D3DVIEWPORT8{
DWORD X;
DWORD Y;
DWORD Width;
DWORD Height;
float MinZ;
float MaxZ;
} D3DVIEWPORT8;

En en nuestro pequeño programa el viewport y la ventana tienen el mismo tamaño, pero en algunos casos no tiene porque se asi (como en el dibujo anterior, puedes hacer regiones de clip para renderizar solo algunas partes) Asi que si tiene el mismo tamaño que la ventana, igual podriamos haber hecho m_d3dsdBackBuffer.Width y m_d3dsdBackBuffer.Height ya que el viewport tiene las mismas dimensiones que nuestro backbuffer. (el backbuffer lo hemos utilizado tambien antes para calcular el "aspect-ratio" de la matriz de proyeccion)

Render()

Esta funcion se llama una vez por frame. Aqui ponemos lo que queremos dibujar. Veamos el codigo:

HRESULT CMyD3DApplication::Render()
{
	// Begin the scene
  	if( SUCCEEDED( m_pd3dDevice->BeginScene() ) )
   	{
   	m_pd3dDevice->SetStreamSource( 0, m_pVB, sizeof(CUSTOMVERTEX) );
   	m_pd3dDevice->SetVertexShader( D3DFVF_CUSTOMVERTEX );
   	m_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLEFAN, 0, 2 );
 	// End the scene.
   	m_pd3dDevice->EndScene();
	}
	return S_OK;
}
 

Basicamente lo que hacemos es llamar a BeginScene() y EndScene(). Cuando le decimos a Direct3D que estamos listos para dibujar la escena con la funcion BeginScene() debemos comprobar que el tambien lo esta. BeginScene()puede devolver D3DERR_SCENE_NOT_IN_SCENE si algo no va bien. Despues de dibujar llamamos a EndScene() para que Direct3D sepa que hemos terminado de dibujar.

>Vale tio, pero y como dibujamos? en que consiste dibujar?

pues "dibujar" va a consistir en dibujar los triangulos que tengamos definidos en nuestro VertexBuffer. simple no?

Primero le decimos que VertexBuffer queremos usar (es posible que tuvieramos varios, o incluso que desearamos mezclarlos). esto se hace con SetStreamSource(). Este es el formato de la funcion:

HRESULT SetStreamSource(
UINT StreamNumber,
IDirect3DVertexBuffer8* pStreamData;
UINT Stride
);

Despues especificamos el VertexShader que vamos a usar, en este caso ya lo teniamos creado y es D3DFVF_CUSTOMVERTEX.

Por ultimo la funcion mas importante que es DrawPrimitive(). El primer parametro indica que tipo de cosa queremos dibujar, tienes un monton...

typedef enum _D3DPRIMITIVE {
D3DPT_POINTLIST=1,
D3DPT_LINELIST=2,
D3DPT_LINESTRIP=3,
D3DPT_TRIANGLELIST=4,
D3DPT_TRIANGLESTRIP=5,
D3DPT_TRIANGLEFAN=6,
D3DPT_FORCE_DWORD=0X7fffffff,
} D3DPRIMITIVETYPE;

En nuestro programa usamos TRIANGLEFAN, que significa que todos los triangulos comparten un vertice.
(el TRIANGLE_STRIP se diferencia del TRIANGLEFAN en que solo los triangulos estan unidos)

InvalidateDeviceObjects()

Tachan!!! fin del programa.... vaciar el vertex buffer y se acabo:

HRESULT CMyD3DApplication::InvalidateDeviceObjects()
{
	SAFE_RELEASE( m_pVB );
  	return S_OK;
}