Documente Academic
Documente Profesional
Documente Cultură
UNIVERSIDAD DE SALAMANCA
INTRODUCCIÓN A DIRECT3D
1. HISTORIA ........................................................................................................................1
1. HISTORIA
Pero en lugar de desarrollar su propia API desde cero, Microsoft hizo un astuto
movimiento, y es que si Bill Gates es bien conocido es por su gran capacidad como
empresario, no siendo la primera vez en la historia de la empresa en que se lleva a cabo
algo similar: Microsoft adquirió un motor 3D en desarrollo y lo integró en DirectX.
Durante la década de los 90, una importante parte de los motores 3D para ordenador
se desarrollaban en Gran Bretaña. Cabe destacar empresas como RenderWare, Argonaut
y su conocidísimo motor “BRender” (el cual fue portado en 1994 al sistema operativo
OS/2) u otras como la pequeña empresa RenderMorphics. Esta última fue fundada en
1993 por Servan Keondjian, Kate Seekings y Doug Rabson, y fueron los responsables
del desarrollo de un producto denominado “Reality Lab”. Fue Keondjian quien
desarrollo el motor durante el día mientras tocaba el piano en una banda durante la
noche. Mientras tanto, Seekings obtuvo un master en gráficos por computador en la
universidad de Middlesex, presentando como proyecto la librería 3D desarrollada por el
equipo, y que curiosamente suspendió por no haberse ceñido a las especificaciones del
mismo. RenderMorphics había integrado Reality Lab en su “Game SDK”, y confiaban
en que tendría gran aceptación entre los desarrolladores de juegos, y no se equivocaron.
Durante la presentación de su SDK en 1994 en “SIGGRAPH94”, Microsoft presenció el
evento, finalmente adquiriendo la empresa en Febrero de 1995.
DirectX 1.0 no fue nada aceptado, era muy lento, mal estructurado, muy complejo e
incluso con muchos fallos de programación. La siguiente versión no apareció hasta
1996, la versión 2.0 de DirectX, que no logró integrase en la comunidad de
programadores de juegos.
La versión 7.0 fue la primera que fue altamente aceptada por los desarrolladores de
juegos: funcionaba bien, hacía la programación de juegos razonablemente fácil, y a
mucha gente le empezó a gustar el interfaz. La versión 8.0 introdujo las mayores
mejoras de la historia de Direct3D. Prácticamente se modifico la arquitectura,
fusionando en una misma interfaz la parte 2D del API, “DirectDraw”, y la 3D en una
misma interfaz denominada “DirectX Graphics”, que llevo a un menor uso de memoria
y un modelo de programación más sencillo. Gracias a esos cambios, DirectX fue por
primera vez mas sencillo de usar y mas consistente que hasta el entonces dominante
OpenGL. Esta versión introdujo técnicas como “Point Sprites” (Conjunto de píxeles con
apoyo por hardware en el sistema de partículas para generar efectos de lluvia,
explosiones, nieve…), “3D Volumetric Textures” (texturas con 3 dimensiones,
permitiéndose iluminaciones por píxel, efectos atmosféricos…), una enorme mejora en
la librería “Direct3DX”, introducida en la versión 7.0 y que proporcionan rutinas útiles
y altamente eficientes para, por ejemplo, enumerar configuraciones de dispositivos,
configurarlos, ejecutar en pantalla completa o en ventana de forma uniforme, cambios
de tamaño, cálculos sobre matrices, etc., así como las técnicas de “N-Patches”, o los
famosos “Vertex & Píxel Shaders”, que produjo un cambio radical en la forma de
programar los juegos modernos y su apariencia.
La última que está por llegar en breve (10), incorpora importantes cambios
estructurales que soluciona diversos problemas que presentaba la versión 9 y que
limitaba a los creadores de videojuegos, como por ejemplo el “object overhead”
(sobrecarga de objetos), derivado del cuello de botella introducido por el acceso de la
CPU a la API de DirectX. Otra importante limitación era el uso de un pipeline fijo, lo
cual muchas veces podía desembocar en la subutilización de recursos (por ejemplo,
situaciones en las que el Píxel Shader estaba usado al 100% mientras que el Vertex
Shader estaba completamente inutilizado).
Durante mucho tiempo, el API Direct3D se consideraba muy malo comparado con
OpenGL. Sin embargo, el paso del tiempo y la continua evolución han convertido a
Direct3D en una interfaz de programación de gráficos muy potente y estable, y en
muchos casos va de la mano en lo que se refiere a innovación en el campo de los
gráficos 3D, ya que Microsoft trabaja muy de cerca con las empresas de hardware
gráfico llegando, en casos recientes, a introducir nuevas técnicas antes de que el
hardware lo haga.
Una de las primeras ventajas que proporciona Direct3D es lo que se conoce como
capa de abstracción del hardware o HAL. A parte del controlador del dispositivo
(driver) que se proporciona con el hardware de video, Direct3D proporciona una capa
de software en torno a la tarjeta gráfica, a la que pertenece el HAL.
Por tanto, Direct3D usa el HAL para acceder a la tarjeta gráfica a través del driver, y
se muestra como un dispositivo. Si la tarjeta gráfica soporta Direct3D, entonces
existirá HAL en el computador, y para ello el driver debe ofrecer una interfaz
compatible con DirectX, ciñéndose a unas determinadas características. Cabe destacar
además que si la tarjeta no ofrece ningún tipo de aceleración por hardware, no se podrá
crear el dispositivo HAL.
A día de hoy son muy pocos los que se enfrentan a este reto y prefieren denegar la
ejecución del programa si el hardware no está capacitado, ya que dichas aplicaciones
son muy dependientes de lo que sea capaz de hacer la tarjeta gráfica.
Gracias a esta característica, Microsoft nos garantiza un API muy consistente capaz
de realizar operaciones alternativas cuando el computador no presente hardware
especializado. Esto se traduce en una enorme versatilidad y aislamiento del hardware.
1) Las interfaces de los objetos COM nunca cambian. Es lo que se conoce como
estrategia de versiones: tu objeto puede evolucionar y ofrecer nuevas interfaces, pero
nunca dejará de ofrecer las antiguas, permanecerán para proporcionar una
compatibilidad hacia atrás, garantizando que las aplicaciones siempre funcionarán a
medida que evoluciona.
2) Los objetos COM son independientes del lenguaje usado: sea cual sea el lenguaje
que se use, se podrá hacer uso de los objetos COM, ya que no son más que porciones
de código binario al cual se accede de una forma siempre constate. Esto se logra,
sobre todo, gracias a que los métodos no se llaman directamente, sino a través de una
doble indirección mediante una tabla de punteros a métodos denominada V-Table.
Por ejemplo, una vez declarada una variable de tipo Interfaz Direct3D 9, se inicializa
mediante una llamada a un método (Direct3DCreate9) que devuelve el puntero
a la interfaz de Direct3D a través de la V-Table. Luego, accediendo a través del
puntero, se pueden invocar los diversos métodos de la interfaz.
¿En qué repercute el uso de COM sobre DirectX? Pues se podría decir que en
compatibilidad, sobre todo: si se desarrolló una aplicación en DirectX 3.0 en un ya
obsoleto computador, dicha aplicación funcionará todavía en un computador de última
generación con una tarjeta de video moderna y con la última versión de DirectX, y no
solo con la última, sino con las futuras versiones. Esto se debe a que el interfaz que se
ofrece al programador Direct3D para trabajar no ha desaparecido. En cada nueva
versión lo que se ofrece es una nueva interfaz, pero no se elimina la anterior. Por
ejemplo, la versión 9.0 de DirectX ofrece las siguientes interfaces de Direct3D:
- IDirect3D
- IDirect3D2
- IDirect3D3
- IDirect3D7
- IDirect3D8
- IDirect3D9
Así pues, gracias al modelo COM, se garantiza que toda aplicación que trabaje con
DirectX funcionará en versiones futuras, además de ayudar a encapsular la información
y a proporcionar interfaces consistentes frente a los diversos lenguajes de programación.
Veamos a continuación un ejemplo que corrobore todo lo explicado: para poner una
luz en la escena, llamaremos a través de la interfaz de direct3D al método que se
encarga de ello:
Por lo general, podemos decir que en general, la mayoría de las llamadas a DirectX
en C++ son de la forma NOMBRE_INTERFAZ NOMBRE_METODO.
Para poder hacer uso de los objetos COM de Direct3D, a la hora de programar
tenemos que enlazar las librerías que se van a usar, que son las siguientes:
- d3dx9dt.lib
- d3dxof.lib
- d3d9.lib
- winmm.lib
- dxguid.lib
- comctl32.lib
También se deberá incluir los ficheros de cabecera, con los prototipos de los
métodos, definiciones de tipos, etc. Todas estas librerías se proporcionan en el SDK de
DirectX creado por Microsoft, que se puede encontrar en www.microsoft.com.
Los vértices son la base de los gráficos 3D, y se usan para definir nuestros objetos
en el espacio. Se definen como vectores libres cuyo origen es el origen del sistema de
coordenadas, y el final es el propio vértice. Los vectores no solo se usan para definir
vértices, también se usan para las iluminaciones o para indicar una dirección. En
Direct3D un vector se define como un tipo de dato denominado D3DVECTOR, y consta
de las componentes, x, y, z. En el caso de los vértices, estos se definen como algo más
que un vector que indica su posición. La estructura típica del tipo de dato Vertex en
Direct3D es similar a la siguiente:
Struct Vertex {
D3DVECTOR vPosition;
DWORD dwDiffuse;
D3DVECTOR vNormal;
FLOAT u, v;
}
El primer atributo indica el vector posición, el segundo un valor que indica el color
del píxel, el tercer valor es el vector normal (necesario para las iluminaciones, como por
ejemplo, la técnica del sombreado gouraud) y el cuarto valor especifica las coordenadas
de una textura para lo que se conoce como mapeado de texturas (texture-mapping).
Direct3D trabaja con vértices de esta forma, y los aloja en un buffer de vértices
(vertex-buffer) al que podrá acceder nuestro programa de aplicación, cuando
normalmente solo podía acceder el driver de la tarjeta gráfica.
El usuario podrá definir qué estructura tendrá el tipo de dato vértice, en función de la
información que nos interese alojar: si no vamos a usar iluminaciones, no nos interesará
las normales, y tampoco incorporaremos las coordenadas de las texturas si no van a ser
usadas. Así logramos optimizar el uso de memoria, que es crítico, pero en cada función
es necesario especificar que estructura tiene el vértice. Esto es una práctica habitual que
se realiza a través de banderas y macros ya definidas en las cabeceras de DirectX.
Cabe destacar que Direct3D hace uso de una técnica conocida como “BackFace
Culling” de forma automática, que consiste en no renderizar aquellos triángulos que no
se ven en la escena, como los que están fuera de la visión de la cámara o aquellos que
no puede ver el espectador porque están detrás de otro elemento. Esto se calcula
obteniendo el vector normal que define el plano de dos lados comunes de dos triángulos
y determinando si el vector está orientado “saliendo de la pantalla” (se renderizará) o
“entrando en la pantalla” (no se renderizará). Así se acelera enormemente la
renderización de una escena.
Una visión de alto nivel de cómo trabaja Direct3D podría ser la siguiente:
- Geometry Processing: Aquí se aplican una amplia diversidad de técnicas sobre los
vértices ya transformados, como por ejemplo el “Back-Culling”, evaluación de los
atributos de los vértices, clipping (relacionado con el buffer de profundidad),
rasterización (proceso por el cual transforma los vértices a píxeles con coordenadas
de dispositivo, solventando los problemas de no concordancia entre las coordenadas
del vértice y del píxel), etc.
- Píxel Rendering: Se aplican las últimas transformaciones sobre los píxeles, como
por ejemplo, aplicar transformaciones con la información de profundidad (z-buffer),
niebla, alfa, stencil buffer…y un gran conjunto de técnicas que producen finalmente
los píxeles para mostrar en pantalla.
5. TUBERÍA DE TRANSFORMACIONES
};
float m[4][4];
};
} D3DMATRIX;
D3DXMATRIX *pOut,
FLOAT Angle
);
D3DXMATRIX *pOut,
FLOAT Angle
);
D3DXMATRIX *pOut,
CONST D3DXVECTOR3 *pV,
FLOAT Angle
);
D3DXMATRIX *pOut,
FLOAT Yaw,
FLOAT Pitch,
FLOAT Roll
);
D3DXMATRIX *pOut,
FLOAT sx,
FLOAT sy,
FLOAT sz
);
D3DXMATRIX matWorld;
D3DXMatrixTranslation(&matWorld, 0,1,0);
D3DXMATRIX matRotateX;
D3DXMatrixRotationY(&matRotateY, 1.57);
D3DXMatrixMultiply(&matWorld, & matRotateY, & matWorld);
...
...
7. TRANSFORMACIONES DE VISTA
Hay diversas formas de crear una matriz de vista, pero en todos los casos, la cámara
tendrá una posición y orientación dentro del espacio del mundo que tomamos como
referencia inicial para su creación. Una primera forma es combinar matrices de
traslación y rotación para la orientación de cada una de las tres direcciones de nuestro
nuevo sistema de referencia. Para facilitar esta complicada labor, las librerías D3X
proporcionan unas funciones para la creación de dichas matrices.
D3DXMATRIX *pOut,
CONST D3DXVECTOR3 *pEye,
CONST D3DXVECTOR3 *pAt,
CONST D3DXVECTOR3 *pUp
);
Ejemplo:
D3DXMATRIX out;
D3DXVECTOR3 eye(2,3,3);
D3DXVECTOR3 at(0,0,0);
D3DXVECTOR3 up(0,1,0);
D3DXMatrixLookAtLH(&out, &eye, &at, &up);
m_pd3dDevice -> SetTransform(D3DTS_VIEW, &out);
8. TRANSFORMACIONES DE LA PROYECCIÓN
Toda proyección definirá un volumen fuera del cual los objetos no son vistos por el
espectador. Este espacio podría entenderse como el recinto existente entre dos planos:
uno cercano y otro lejano. Estableciendo la forma y tamaño de estos planos podemos
lograr cambiar la relación de aspecto de la escena a proyectar y el ángulo de visión que
tenemos de la misma escena.
Las funciones proporcionadas para crear la matriz de proyección son las siguientes:
D3DXMATRIX *pOut,
FLOAT w,
FLOAT h,
FLOAT zn,
FLOAT zf
);
Donde pOut estable el puntero a la matriz de salida, w es el ancho del plano más
cercano que define el recinto, y h su ancho. Zn definirá la profundidad a la que se
encuentra el plano más cercano, y zf la del plano más lejano. Para esta función existe
su variante dextrógiro, D3DXMatrixPerspectiveRH. Efectúa una proyección
ortogonal y es equivalente a la función D3DXMatrixPerspectiveOffCenterLH.
D3DXMATRIX *pOut,
FLOAT fovy,
FLOAT Aspect,
FLOAT zn,
FLOAT zf
);
D3DXMATRIX *pOut,
FLOAT l,
FLOAT r,
FLOAT b,
FLOAT t,
FLOAT zn,
FLOAT zf
);
Ejemplo:
D3DXMATRIX matProj;
D3DXMatrixPerspectiveFovLH( &matProj, D3DX_PI/4, 1.0f,
1.0f, 100.0f );
g_pd3dDevice->SetTransform( D3DTS_PROJECTION, &matProj );
Este ejemplo genera una matriz con un ángulo de visión de PI cuartos, una relación
de aspecto de 1 (uno de alto por uno de ancho), un volumen definido entre Z = 1 y Z =
100, y luego la establece como matriz de proyección mediante la constante
D3DTS_PROJECTION.
X e Y definen las coordenadas del píxel de la esquina superior izquierda del puerto
de vista sobre el dispositivo de renderización. Width especifica su ancho y Height su
alto, y los parámetros MinZ y MaxZ referencian a los valores mínimos y máximos a
usar en lo referente a profundidad. Todos estos parámetros describen el rectángulo del
puerto de vista.
Una vez creada la matriz del puerto de vista, se le pasa como argumento al método
SetViewport() del dispositivo, y tomará efecto la próxima vez que se renderice la
escena. Si no se especifica un puerto de vista, Direct3D tomará por defecto todo el
dispositivo de renderización que se le haya asignado.
HRESULT Clear(
DWORD Count,
const D3DRECT *pRects,
DWORD Flags,
D3DCOLOR Color,
float Z,
DWORD Stencil
);
Por lo general, se pondrán toda la estructura a cero, para establecer las propiedades
menos usadas a su valor por defecto, y luego se establece aquellas características cuya
elección si es crítica.
HRESULT CreateDevice(
UINT Adapter,
D3DDEVTYPE DeviceType,
HWND hFocusWindow,
DWORD BehaviorFlags,
D3DPRESENT_PARAMETERS *pPresentationParameters,
IDirect3DDevice9 **ppReturnedDeviceInterface
);
- Adapter indica cual de los dispositivos de vídeo (varias tarjeta gráficas) usar.
Normalmente se pondrá a D3DADAPTER_DEFAULT, que siempre es el primer
dispositivo.
Los buffers de vértices son porciones de memoria que alojan vértices (en forma de
arrays que representan un vértice). Estos vértices pueden estar transformados o no, y se
podrán procesar para realizar una transformación, iluminaciones, etc. Estos buffers se
suelen alojar en la memoria de video, desde donde se pueden efectuar los cálculos 3D
mucho más rápido. Por ejemplo, podríamos almacenar un determinado modelo, efectuar
todas las transformaciones necesarias sobre el y luego representar la escena tantas veces
como se desee sin tener que volver a efectuar las transformaciones.
#define D3DFVF_CUSTOMVERTEX
(D3DFVF_XYZ|D3DFVF_DIFFUSE|D3DFVF_TEX1)
typedef struct
{
D3DVALUE x,y,z;
D3DVALUE diffuse;
D3DVALUE u,v;
} VERTICE;
HRESULT CreateVertexBuffer(
UINT Length,
DWORD Usage,
DWORD FVF,
D3DPOOL Pool,
IDirect3DVertexBuffer9** ppVertexBuffer,
HANDLE* pSharedHandle
);
Ejemplo:
d3dDevice->CreateVertexBuffer(3*sizeof(VERTICE),0,
D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &g_pVB, NULL)
Para poder acceder al buffer, primero hay que bloquearlo. Para ello se usa la función
IDirect3DVertexBuffer9::Lock, para luego rellenar el buffer con los vértices
(mediante un simple memcpy en el caso de trabajar en C) o bien leer su contenido. Esta
función acepta cuatro parámetros:
HRESULT Lock(
UINT OffsetToLock,
UINT SizeToLock,
VOID **ppbData,
DWORD Flags
);
VOID* pVertices;
if (FAILED(m_pVB->Lock(0, m_dwSizeofVertices,
(BYTE**)&pVertices,0)))
return E_FAIL;
memcpy( pVertices, cvVertices, m_dwSizeofVertices);
m_pVB->Unlock();
Como era de esperar, tras finalizar el uso de un buffer de vértices, habrá que
desbloquearlo a través de IDirect3DVertexBuffer9::Unlock(). No tiene
ningún parámetro.
Para dibujar una escena a partir de un conjunto de vértice, en primer lugar se debe
especificar la fuente de los mismos, para lo cual se usa la función
IDirect3DDevice9::SetStreamSource().
HRESULT SetStreamSource(
UINT StreamNumber,
IDirect3DVertexBuffer9 *pStreamData,
UINT OffsetInBytes,
UINT Stride
);
Por ejemplo:
Indica que el puntero al buffer g_pVB contiene elementos del tamaño de los vértices
que definimos como VERTICE.
El siguientes pasos para el dibujo de una escena sería indicar el “vertex shader” a
usar. Habrá que indicarle el formato del vértice con el que estamos tratando mediante el
método SetVertexShader():
d3dDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
FIGURA 6: TRIANGLEFAN
FIGURA 7: TRIANGLESTRIP
if( SUCCEEDED(d3dDevice->BeginScene())) {
d3dDevice->SetStreamSource(0, m_pVB, sizeof(VERTICE));
d3dDevice->SetVertexShader(D3DFVF_CUSTOMVERTEX );
d3dDevice->DrawPrimitive(D3DPT_TRIANGLEFAN, 0, 2);
d3dDevice->EndScene();
}
HRESULT CreateIndexBuffer(
UINT Length,
DWORD Usage,
D3DFORMAT Format,
D3DPOOL Pool,
IDirect3DIndexBuffer9** ppIndexBuffer,
HANDLE* pSharedHandle
);
La creación de un buffer de índices exige de una serie de parámetros que defina sus
características.
Length indica el tamaño que tendrá el buffer, en bytes. Usage concreta el uso que
se le dará al buffer, concretando determinadas compatibilidades, por ejemplo, que el
buffer sea usado por software en lugar de hardware
(D3DUSAGE_SOFTWAREPROCESSING). Format indica el tipo de dato que será el
índice. Al igual que con el buffer de vértices, habrá que especificar donde y como se
alojará la información de los índices, a través de Pool, para lo cual se recomienda
relegar esa decisión a Direct3D con D3DPOOL_MANAGED. ppIndexBuffer será
nuestro puntero al buffer, y el último parámetro es nuevamente reservado para el
sistema.
Una vez creado, el acceso al buffer exigirá de nuevo el uso de los métodos Lock()
y Unlock() para bloquearlo y poder acceder a el, de forma similar a como hacíamos
con el buffer de vértices.
LPDIRECT3DINDEXBUFFER9 m_pIB;
WORD dwIndices[] = {0, 1, 2, 0, 2, 3};
m_dwSizeofIndices = sizeof(dwIndices);
if(FAILED(d3dDevice->CreateIndexBuffer(m_dwSizeofIndices,
0, D3DFMT_INDEX16, D3DPOOL_MANAGED, &m_pIB ) ) )
return E_FAIL;
VOID* pIndices;
if(FAILED(m_pIB->Lock( 0, m_dwSizeofIndices,
(BYTE**)&pIndices, 0)))
return E_FAIL;
memcpy( pIndices, dwIndices, sizeof(dwIndices) );
m_pIB->Unlock();
Ahora, para dibujar las primitivas, habrá que establecer en primer lugar el índice que
estamos usando mediante SetIndices(), y en segundo lugar sustituir el uso de la
función DrawPrimitive() por DrawIndexedPrimitive().
Gracias al uso del COM, Microsoft ha garantizado un sistema que a pesar de estar en
continua evolución, garantizará el funcionamiento de aplicaciones desarrolladas con
versiones antiguas del API. Sin embargo, su uso puede asustar al comienzo, ya que las
cabeceras COM no son precisamente sencillas de entender, aunque el paso del tiempo
han permitido que los desarrolladores tengan a su alcance una estructura y sintaxis de
programación mas intuitiva y sencilla. Además, aquellos desarrolladores acostumbrados
a la programación orientada a objetos se sentirán cómodos con Direct3D.
Y lo cierto es que Direct3D hace las cosas muy bien, y a causa de estar diseñado para
Windows, lo hace ideal para computadores muy variados, desde sistemas de pocos
recursos hasta computadores con lo último de hardware gráfico. Los profesionales de
los gráficos 3D preferirán una plataforma UNIX o de Sillicon Graphics.
Comentar también que no hay nada que Direct3D haga y OpenGL no pueda hacer,
pero tienen una cierta ventaja a causa de que las últimas características del hardware
gráfico necesitarán código personalizado para cada adaptador en OpenGL, mientras que
Direct3D ya estará implementado. No obstante, Direct3D resultará más complicado a la
hora de programar, porque no oculta totalmente los elementos de bajo nivel como hace
OpenGL, pero esto también desemboca en una pérdida de flexibilidad a la hora de
programar. Será el programador el que deba decidir entre flexibilidad o simplicidad. En
lo referente a flexibilidad, cabe destacar las dos mayores fuerzas de Direct3D en lo que
a programación se refiere: “Programable Píxel & Vertex Shaders”, porciones de código
propio similar al ensamblador que se inyectan en la tubería de renderización para
procesar los vértices y píxeles finales que se mostrarán en pantalla.
- No es portable
Una de las principales dificultades que presenta son las extensiones, que son muy
dependientes de hardware especifico, o también su elevado número de nombres
diferentes para una misma función (Direct3D aprovecha la sobrecarga de nombres de
función propias de la programación orientada a objetos). Tampoco tiene mucho soporte
para todos los lenguajes de programación.
15. BIBLIOGRAFÍA
- Artículo: Direct3D vs. OpenGL: Which API to Use When, Where, and Why
by Promit Roy, GameDev.net
- http://www.programmersheaven.com/
- http://www.gamedev.net/
- http://www.chilehardware.com/guias_guia067-20061024.html