DirectX. Si has llegado a este artículo por que quieres programar tu propio Quake 4 en tus ratos libres deja que te saque de tu error, jamás vas a poder hacerlo. Antiguamente los juegos los programaban uno o dos programadores que trabajaban unos meses y conseguían crear un juego (mejor o peor). Actualmente los juegos los programan equipos relativamente grandes de personas con diversas especialidades (no hace falta solo programar sino también diseño gráfico, composiciones musicales, efectos de sonido, etc).
¿Entonces que voy a poder hacer? Bueno, quizá no consigas programar un juego de última generación pero quizá si puedas programar algún pequeño juego, conseguir un simulador físico convincente o sencillamente impresionar a las visitas (y de paso mejorar tu curriculum). Además programar gráficos en 3D es bastante gratificante (una vez las cosas van funcionando) puesto que el resultado es más "gráfico".
Managed DirectX se traduce como DirectX "Asistido" que me parece un termino muy feo en español así que simpre utilizaré el termino ingles que me suena mucho más apropiado.
DirectX es complicado y relativamente dificil de explicar, en este artículo vamos a empezar con unos cuantos conceptos generales y un ejemplo de código completo. Lo más normal suele ser empezar por el ejemplo más simple que hay, sin luces, ni meshes y sin tocar las matrices demasiado sin embargo yo prefiero poner un ejemplo completo con todas las cosas básicas y explicar (aunque algunas sea solo por encima) todas las cosas que se utilizan de forma que os familiariceis cuanto antes con los conceptos básicos.
¿Por que programar en DirectX en vez de en OpenGL? No hay ninguna razón especial exceptuando la comodidad. En C# es muy sencillo usar DirectX gracias a las extensiones incluidas en el SDK sin tener que importar librerias extrañas, en cambio para programar con C# en opengl hay que hacer más cosas (importar ciertas librerias, al menos la última vez que lo intente) pero eso es todo, las diferencias entre ambos son relativamente pequeñas (en las cosas básicas al menos) y si sabes programar usando las librerías de DirectX no te será muy dificil adaptarte a OpenGL (y viceversa). Si por alguna razón lo que estás buscando es un manual de opengl hay numeroso recursos en la página de Neon Helium que tiene varias guías de OpenGL (en ingles eso si).
Igual que en el caso anterior por una cuestión de comodidad. C# proporciona algunas facilidades al programador sobre C++ .NET como son el recolector de basura o el hecho de que sea intrinsecamente threadsafe. Hay algunas comparativas de rendimiento entre C# y C++ con DirectX y el rendimiento del segundo es algo superior pero, para lo que nosotros vamos a poder hacer a nivel personal dicha diferencia de rendimiento casi no se va a notar (a parte del hecho de que generalmente las malas prácticas de programación son lo que más afecta al rendimiento de un programa).
En este primer tutorial vamos a empezar por crear lo típico, un cubo en pantalla al que haremos rotar sobre si mismo de forma que introduciremos algunos conceptos como el de mesh, el proceso de pintado o el de inicialización del DirectX.
El proceso de dibujado en MDX (Managed DirectX) es relativamente sencillo aunque puede realizarse de diversas formas (en este caso vamos a utilizar el evento OnPaint del formulario).
En general el procedimiento general sigue más o menos estos pasos:
Con la versión nueve de DirectX Microsoft introdujo el concepto de Managed DirectX (DirectX asistido) frente a la programación directX tradicional.
Managed DirectX constituye una integración de la funcionalidad DirectX como parte de las librerías .NET (a través del SDK de DirectX) facilitando en cierto sentido ciertas de las operaciones más comunes (y proporcionando parte de las ventajas de .NET como la seguridad de ejecución). La mayor parte de las operaciones son muy similares.
Para poder utilizar la funcionalidad de DirectX debemos referenciar las librerías de DirectX adecuadas. Existen varios namespaces con distinta funcionalidad (no es el objetivo de este tutorial hablar sobre assemblys y namespaces de forma que si no sabes lo que son echale un vistazo antes a este otro artículo de breve introducción a .NET).
Hay varias librerías dentro del namespace DirectX (Microsoft.DirectX, Microsoft.DirectX.DirectSound, Microsoft.DirectX.Direct3D) que encapsulan la funcionalidad equivalente a sus nombres. Para este tutorial vamos a utilizar tipos y clases ubicadas en Microsoft.DirectX (que contiene las clases comunes) así como en Microsoft.DirectX.Direct3D y Microsoft.DirectX.Direct3DX (está última nos proporciona la clase Mesh y varias clases derivadas) por lo que deberemos incluirlas como referencias en nuestro proyecto.
Antes de poder utilizar las funciones de DirectX para dibujar objetos necesitaremos inicializarlo, que, en resumen, es equivalente a obtener un device context que es, por describirlo de alguna forma, el lienzo en el que pintaremos.
El device (traducido dispositivo) constituye una abstracción de manejador del dispositivo físico que realiza el dibujado (la tarjeta gráfica) que nos presenta una serie de interface que por debajo estarán implementados de una determina forma y usando las capacidades concretas de la tarjeta gráfica que haya debajo (esto es muy similar a los origenes abstractos de datos pero a la inversa).
La descripción del constructor de un dispositivo es:
Crear un device un procedimiento muy sencillo en Managed DirectX (aunque pueden introducirse más opciones de configuración, pero estas son las básicas).
// Creamos el device (dxDevice es un miembro privado)
dxDevice = new Device(0, DeviceType.Hardware, dxForm,
CreateFlags.SoftwareVertexProcessing, pParameters);
// Hacer que solo se pinte en el evento OnPaint
// para evitar pintados automáticos
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.Opaque,true);
}
Vamos a realizar el proceso de dibujado en el evento OnPaint del formulario, hay otras formas de realizar el pintado (bastante mejores) pero este metodo tiene la ventaja de ser sencillo.
Aunque no es totalmente necesario un conocimiento rudimentario de algebra es recomendable para entender mejor las transformaciones que se producen en DirectX. Hay numerosos parametros de configuración en el device que nos permiten configurar las luces, efectos de pixel shader, etc. Para este ejemplo nos interesa observar las tres matrices de transformación. La matriz de proyección, la matriz de vista y la matriz de mundo (Projection, View y World por sus nombre en inglés).
Estas tres matrices van a definir lo que dibujamos:
El proceso de dibujado en directX es sencillo. Lo primero que debemos hacer es limpiar la escena, es decir, comenzar con un lienzo en blanco (o de hecho con el color de fondo que prefiramos), una vez limpiada la escena definimos la situación de las luces presentes en la escena (puesto que si no hay luces no se verá nada), por último definimos los objetos de nuestra escena.
La definición de dichos objetos deberemos realizarla entre dos llamadas a dos metodos de DirectX: BeginScene y EndScene. Además, en el código que sigue aparecen algunas variables que estan definidas como miembros privados como son el caso de la variable angle que es un float que representa el giro (rotación) actual de los dos cubos que vamos a dibujar, el dxDevice que lo habremos creado durante la inicialización del DirectX y por ultimo un boxMaterial que será el que nos permita definir el color de los cubos.
public DXForm()
{
//
// Necesario para admitir el Diseñador de Windows Forms
//
InitD3D();
cubeMesh = Mesh.Box(dxDevice,2.0f,2.0f,2.0f);
}
private void DXForm_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
// Limpiar el device y el ZBuffer usando un color azulado
dxDevice.Clear(ClearFlags.Target | ClearFlags.ZBuffer,
System.Drawing.Color.CadetBlue,
1.0f,0);
// Definir la matriz de proyección
dxDevice.Transform.Projection = Matrix.PerspectiveFovLH((float) Math.PI / 4,
this.Width / this.Height,1.0f,100.0f);
// Definir la camara
dxDevice.Transform.View = Matrix.LookAtLH(new Vector3(0.0f,0.0f,10.0f),
new Vector3(0.0f,0.0f,0.0f),
new Vector3(0.0f,1.0f,0.0f));
// Ir rotando el primer cubo y situarlo en -2,0,0
dxDevice.Transform.World = Matrix.RotationYawPitchRoll(this.angle, this.angle,this.angle) *
Matrix.Translation(-2.0f,0.0f,0.0f);
// Comenzamos a pintar la escena
dxDevice.BeginScene();
// Asignar un material a nuestro cubo (color blanco)
boxMaterial.Ambient = Color.White;
boxMaterial.Diffuse = Color.White;
dxDevice.Material = boxMaterial;
// Calcular las normales del cubo
cubeMesh.ComputeNormals();
// Dibujar el cubo
cubeMesh.DrawSubset(0);
// Ir rotando el segundo cubo y situarlo en 2,0,0
dxDevice.Transform.World = Matrix.RotationYawPitchRoll(-this.angle,this.angle,this.angle) *
Matrix.Translation(2.0f,0.0f,0.0f);
// Asignar el material a nuestro segundo cubo (rojo)
boxMaterial.Ambient = Color.Red;
boxMaterial.Diffuse = Color.Red;
dxDevice.Material = boxMaterial;
cubeMesh.ComputeNormals();
cubeMesh.DrawSubset(0);
dxDevice.EndScene();
// Dibujamos la escena, actualizamos el angulo e invalidamos para volver a pintar
dxDevice.Present();
this.angle += 0.01f;
this.Invalidate();
}
El proceso de dibujado en DirectX resulta poco intuitivo al principio. El proceso de dibujado que todos consideramos intuitivo cuando dibjuamos un cubo en una determinada posición es, sencillamente dibujarlo en dicha posición, es decir de alguna forma decir, el cubo va a estar en esta posición, esta girado este determinado ángulo y tiene este determinado color.
Sin embargo en DirectX las cosas no son así. Haciendo un simil podríamos decir que el device es nuestro conjunto de recursos que incluye el lienzo sobre el que dibujamos, nuestra paleta de materiales, algunos patrones de colores (texturas), las luces, las camaras... (la claqueta XD). Por otro lado estan los objetos que podemos dibujar, cubos, esferas, objetos dibujados vertice a vertice, diseños en formato .x (meshes creadas por ejemplo con otras aplicaciones de diseño 3D) y dichos objetos se dibujan de una forma un tanto peculiar.
Si queremos dibujar un cuadrado (por ser uno de los ejemplo más sencillos) cuyo centro este en el punto (0,0,0) (es decir en el centro justo de la pantalla) y de dos unidades de longitud, tendremos que situar sus vertices en las siguientes coordenadas. (-1,1,0) Arriba izquierda, (-1,-1.0) Abajo izquierda, (1,-1,0) Abajo derecha, (1,1,0) Arriba derecha, pero es como si nuestro "lápiz" tan solo pudiera poner o quitar un punto, es decir, podemos imaginar nuestro lápiz suspendido sobre el lienzo y totalmente inamobible de forma que lo único que podemos hacer es ponerlo sobre el lienzo para pintar o quitarlo.
Ante esta situación lo que realmente hacemos para pintar nuestro cuadrado, puesto que no podemos mover el lapiz es, sencillamente desplazar el lienzo, si queremos pintar en el punto (2,0,0) lo que realmente hacemos es mover el lienzo en la dirección contraria, es decir, movelo a (-2,0,0) de forma que el punto (2,0,0) queda justo debajo de nuestro hipotético pincel y entonces pintar. De la misma forma no es el objeto el que es de un determinado color sino que, cuando vamos a pintarlo, asignamos el material correspondiente a nuestro pincel y entonces pintamos (es como pintar con lapices de colores).
Para poder ver algo en nuestra escena debemos tener al menos una luz. Las luces, al contrario que los objetos de la escena no es necesario definirlas cada vez que pintamos sino que se definen una sola vez y permanecen en la posición y de la forma definida mientras no los cambiemos. Las luces se definen mediante un array (Lights) de objetos de tipo Light,
Con la llamada a clear limpiamos la pantalla, todo lo que hubieramos pintado en el frame (fotograma) anterior se descarta. El primer parametro indica que es lo que queremos limpiar, en este caso el lienzo (target) y z-buffer (el buffer de profundidad). Estos dos parametros son necesarios practicamente siempre al comienzo del bucle de dibujado para limpiar tanto los objetos anteriormente dibujados como la información de profundidad de dichos objetos.
Una vez hecho esto comenzamos el dibujado de la escena.
Con estas llamadas ajustamos las distintas matrices de transformación. Para una visión detallada de lo que esto significa recomiendo que le echeis un vistazo al artículo [url=""]el Pipeline de DirectX[/url]. En lineas generales estamos ajustando tres "parametros" del device que nos servirán para ajustar como vemos la imagen.
El primero de ellos define la proyección de la vista, esto es equivalente a definir la persepectiva de la camara, también conocida como el FOV (Field of view, o campo de visión). Estos parametros van a definir si percibimos la imagen en isométricas, en caballeras, etc ... Si habeís visto las típicas peliculas en las que se ve un pasillo que de repente se alarga hasta llegar al infinito, ese efecto se consigue cambiando precisamente estos valores (pero en una camara de verdad). Lo más normal es dejarlos en los parametros de arriba que consisten en una vista en isométricas (no estoy totalmente totalmente seguro de esto, pero si bastante seguro xD). Se define mediante una llamada a la función estática PerspectiveFovLH de la clase matriz que viene a ser algo así como Matriz de perspectiva FOV para mano izquierda (LH = Left Hand)
El segundo parametro define nuestra camara. Para ello vamos a utilizar otra función estática de la clase Matriz llamada LookAtLH (mirar a, mano izquierda) al que le pasamos tres vectores. El primero indica la posición de la camara, es decir, el sitio donde esta ubicada la camara. El segundo indica la posición del objetivo de la camara, es decir, el lugar hacia el que mira la camara, el tercero indica la orientación de la camará, es decir, hacia donde apunta su parte de arriba. Para hacer un simil podríamos equipararlo a si tenemos la camara normal, cogida de lado, boca abajo ...
Para nuestro ejemplo definimos la camara mirando al centro del universo :) (0,0,0) y situada con un desplazamiento de 10 en el eje Z, es decir, teniendo en cuenta que el eje Z mide la profundidad y que negativo es hacia dentro y positivo hacia fuera, la estamos situando más o menos en frente del monitor, cerca de donde estamos sentados.
Por último definimos las matriz de mundo. Está matriz define el punto en el que dibujaremos nuestro objeto (el simil hecho anteriormente es más o menos adecuado, pero repito, para una explciación correcta mejor leeros el artículo [url=""]el Pipeline de DirectX[/url]. De esta forma formamos esa matriz mediante la combinación (multiplicación) de dos matrices distintas, una de rotación (que es lo que hace que roten nuestros cubos) y otra de traslación (que nos permite hacer que nuestros cubos aparezcan un poco más a la derecha o un poco más a la izquierda del centro de la pantalla). Esto deberemos hacerlo cada vez que definamos un objeto ya que es la que define la posición y orientación del objeto.
Una vez definidas nuestras mátrices es hora de pasar al dibujado real de los dos cubos.
En primer lugar asignamos el material (el color en este caso) que queramos que tenga nuestro cubo al dxDevice (recordemos que esto es algo así como mojar el pincel).
Despues realizamos la acción de calcular las normales del objeto (que no explicaré en detalle aqui pero que es necesario realizarlo para que las luces incidan correctamente sobre el objeto).
Por último mediante la llamada al metodo DrawSubset(0) estamos pintando realmente el objeto en la pantalla con todos los parametros anteriormente definidos. Un objeto, un mesh, puede tener varios subset que definen cada uno de los grupos de caras que tienen atributos distintos (distintas texturas, distintas propiedades...), en este caso puesto que tenemos un cubo sencillo con un material plano tan solo tenemos un set.
Ahora volvemos a asignar la matriz de transformación para dibujar el segundo cubo repitiendo los pasos anteriores pero cambiando en la matriz de transformación la posición.
Por último llamamos al metodo endScene() para indicar que hemos acabo de dibujar la escena.
Realmente las transformaciones en las matrices de proyección, camara y mundo no tienen por que estar entre el beginScene y el endScene puesto que solo definen las transformaciones que se llevarán a cabo cuando se dibuje la escena. Especialmente cierto es en la camara puesto que si esta no se mueve no hay necesidad de reajustar la matriz (y de hecho es un gasto tonto de tiempo de CPU) y sobre todo en la matriz de proyección que, exceptuando los momentos en los que queramos hacer efectos con la camara, es dificil que tengamos necesidad de cambiarla.
Por otro lado, en el archivo adjunto hay algunas cosas más de las que se han indicado aqui (aunque no muchas), así que recomiendo abrirlo y echarle un vistazo detallado.
| Attachment | Size |
|---|---|
| DxCube.zip | 12.15 KB |