|
Con este tutorial comenzamos una serie
de tutoriales cuyo fin es desarrollar una escena 3D compleja usando los
componentes de la suite DelphiX.
En este primer tutorial nos familiarizaremos con la
suite DelphiX y crearemos una sencilla aplicación que nos proporcionará
una plantilla básica sobre la que se irán construyendo posteriores
tutoriales.
Comencemos.
El primer paso es crear nuestra
aplicación 3D, para ello procedemos como con cualquier otra aplicación
Delphi (si no estás familiarizado con Delphi te sugiero que empieces con
un tutorial más básico pues voy a dar por sabidas muchas cosas
relacionadas con el IDE y el desarrollo de una aplicación con Delphi).
Una vez tengamos la Ficha (Form) en pantalla, y antes de hacer nada,
salvamos el proyecto. Para el proyecto sugiero el nombre D3D7Ini y para la
unit principal UD3D7Ini, por otra parte son los nombres que encontrará en
los fuentes.
El segundo paso es preparar la ficha para trabajar con
DirectX, para lo cual ponemos en ella el componente DXDraw, que
encontraremos en la pestaña DelphiX de la paleta de componentes si hemos
instalado correctamente la suite.
Poner el componente no es suficiente, aún
hemos de hacer un par de cosas.
La primera es incluir en la cláusula
uses de la sección interface las units: DXDraw, DXClass, y DirectX.
La segunda es transformar la Ficha en una ficha DirectX
con el siguiente cambio:
TForm1 = class(TDXForm)
Es decir hemos de cambiar el ascendiente de la Ficha TForm
por TDXForm
Por último, en el inspector de objetos, seleccionamos
el componente DXDraw y en la propiedad Align seleccionamos alClient.
Ya tenemos una ficha DirectX, sólo falta comprobar que
las opciones activas del componente DXDraw son las correctas. Aprovechando
que ya tenemos en el inspector de objetos el componente DXDraw, buscamos
la propiedad options y nos aseguramos de tenerla igual que en la
imagen1:

(Imagen 1)
Generalmente tendremos la opción do3D a False.
Sólo comentaré las opciones mas importantes:
doFullScreen := False; con este valor nos
aseguramos de trabajar en modo ventana. Si ponemos el valor a True
la aplicación cambiará a pantalla completa.
do3D := True; habilita las opciones de renderizado 3D.
doDirectX7Mode := True; selecciona el driver DirectX 7, necesario
para usar las API DirectX7
doHardware := True; selecciona el renderizado por hardware.
doZBuffer := True; activa el ZBuffer. Si está seguro de que su
tarjeta gráfica soporta el ZBuffer áctive la opción.
Llegados a este punto, hemos de decidir
cómo vamos a controlar el bucle de renderización, bien mediante un cronómetro
(timer) o usando el evento OnIdle de la aplicación. Ambos métodos
tienen sus ventajas y desventajas, como todo en la vida 8D, pero ya que
estamos con Delphix vamos a usar el cronómetro que instala la suite el
cual es considerablemente mas rápido que el que tenemos en la pestaña System
de la paleta de componentes. Así pues ponemos el componente DXTimer
en la ficha. Finalmente, en el inspector de objetos, cambiamos el valor de
la propiedad interval y lo ponemos a 1 para obtener el máximo
rendimiento. También conviene poner la propiedad Enabled a False.
Una vez hecho esto la ficha debería tener el aspecto de la imagen 2:

(Imagen 2)
Puesto que no vamos a usar ningún otro componente de
la suite, ha llegado la hora de empezar a teclear código.
Aprovechando que estamos en el inspector de objetos
seleccionamos el componente Form1 (si le hemos cambiado el nombre en el
inspector de objetos buscamos dicho nombre) y activamos el evento OnCreate
donde pondremos el siguiente código:
procedure TForm1.FormCreate(Sender: TObject);
begin
DXTimer1.Enabled:=False;
If Not(doFullScreen in DXDraw.Options) then
DXDraw.Options:=DXDraw.Options-[doFullScreen];
If Not(do3D in DXDraw.Options) then
DXDraw.Options:=DXDraw.Options+[do3D];
If Not(doDirectX7Mode in DXDraw.Options) then
DXDraw.Options:=DXDraw.Options+[doDirectX7Mode];
If Not(doHardware in DXDraw.Options) then
DXDraw.Options:=DXDraw.Options+[doHardware];
If Not(doZBuffer in DXDraw.Options) then
DXDraw.Options:=DXDraw.Options+[doZBuffer];
end;
Con este código nos aseguramos de que
tenemos las opciones correctas en el componente DXDraw.
En este evento conviene inicializar todas aquellas
variables y estructuras que vayan a tener un papel crítico en la aplicación.
Nótese que en la primera línea del evento
desactivamos el cronómetro. Si no lo hacemos así, el proceso de inicialización
puede verse interrumpido por una llamada al bucle de renderizado lo que,
con toda seguridad, generará un error en tiempo de ejecución.
A continuación activamos el evento OnInitialize
del componente DXDraw donde pondremos el resto de código de inicialización
de nuestra escena. A la salida del evento reactivamos el cronómetro.
procedure TForm1.DXDrawInitialize(Sender: TObject);
begin
// Reactivamos el bucle de renderizado
DXTimer1.Enabled:=True;
end;
En nuestro caso sólo hemos de
reactivar el cronómetro pues no necesitamos mas inicialización básica.
Ya estamos listos para crear el área de visualización,
vista, o viewport (en la medida de lo posible, castellanizaré
todos aquellos términos que resulten confusos en su versión inglesa, así,
si bien viewport puede hacer referencia a toda el área de la aplicación
directx, ya sea en modo ventana, ya sea a pantalla completa, DirectX
permite el uso de múltiples viewports, o vistas, por lo que en una misma
ventana podríamos tener 2, 3, o mas viewports, por lo que la equivalencia
viewport = ventana sólo es válida cuando tenemos sólo uno y sus
dimensiones coinciden con las del área de la aplicación).
El código de inicialización de la vista lo pondremos
en el evento OnInitializeSurface del componente TDXDraw:
procedure TForm1.DXDrawInitializeSurface(Sender:
TObject);
var
Vista : TD3DViewport7;
begin
// Inicializa la vista de la escena
Fillchar(Vista, SizeOf(Vista),0);
With Vista Do
begin
dwx:=DXDraw.Top;
dwy:=DXDraw.Left;
dwWidth:=DXDraw.SurfaceWidth;
dwHeight:=DXDraw.SurfaceHeight;
// para efectos especiales
hacer MinZ=MaxZ
// en cuanquier caso, MinZ y
MaxZ sólo pueden tomar valores entre 0.0 y 1.0
dvMinZ:=0.0;
dvMaxZ:=1.0;
end;
DXDraw.D3DDevice7.SetViewport(Vista);
end;
Como vemos, para inicializar la vista
donde vamos a poner nuestra escena 3D hacemos una llamada a la función SetViewPort
de la propiedad D3DDevice7 de nuestro componente DXDraw. Para ello
hemos de pasar los datos de la vista en una estructura de tipo TD3DViewport7.
Ya que nuestra vista va a ocupar toda el área de la aplicación, la
posición de la esquina superior izquierda de la vista coincide con la
esquina superior izquierda del componente DXDraw, así mismo hacemos que
la altura y la anchura de la aplicación coincida con la del área del
componente DXDraw.
No es obligatorio poner las dimensiones de DXDraw, sin
embargo, hacer que la vista tenga dimensiones superiores a DXDraw tendrá
como efecto dejar fuera del área visible aquellos elementos que no se
rendericen dentro de los límites de DXDraw (lo que no significa que no se
hayan rendirazado), en cambio una vista con menores dimensiones no usará
toda el área disponible para el renderizado. Puesto que no espero que se
fíe de mi palabra, experimente con los valores dwx, dwy, dwWidth,
y dwHeight.
Por último, y como se indica en los comentarios
insertados en el código, los valores MinZ y MaxZ han de
estar en el rango 0.0-1.0. Para renderizar completamente la escena los
valores son los que se han puesto en el código. Sólo en caso de buscar
efectos especiales es útil dar el mismo valor a MinZ y a MaxZ.
Así, si sólo queremos que se rendericen los elementos en el primer plano
igualaremos a 0.0 ambos valores; los igualaremos a 1.0 si queremos que sólo
se rendericen los elementos del fondo (background).
Con la vista inicializada ya podemos poner algo en
nuestra escena. En nuestro caso sólo vamos a poner el fondo de color azul
y mostrar un texto que nos dé los Fotogramas Por Segundo (Frames Per
Second: FPS) y el nombre del ejemplo tutorial.
Para ello vamos a crear nuestro bucle de renderización
en el evento OnTimer del cronómetro:
procedure TForm1.DXTimer1Timer(Sender: TObject;
LagCount: Integer);
var
Rect3D : TD3DRect;
begin
// Funcion de renderizado
If not DXDraw.CanDraw Then
Exit;
// Borra el frame actual (pone la escena de color
azul)
Rect3D.x1:=DXDraw.Top;
Rect3d.y1:=DXDraw.Left;
Rect3D.x2:=DXDraw.SurfaceWidth;
Rect3D.y2:=DXDraw.SurfaceHeight;
// Borramos la escena vieja.
If DXDraw.ZBuffer<>Nil Then
DXDraw.D3DDevice7.Clear(1,
Rect3D, D3DCLEAR_TARGET or
D3DCLEAR_ZBUFFER,
RGB_MAKE(0,0,255), 1.0, 0 )
Else
DXDraw.D3DDevice7.Clear(1,
Rect3D, D3DCLEAR_TARGET,
RGB_MAKE(0,0,255), 1.0, 0 );
// Dibujamos la nueva escena.
DXDraw.D3DDevice7.BeginScene;
DXDraw.D3DDevice7.EndScene;
{ Draw FrameRate }
with DXDraw.Surface.Canvas do
begin
try
Brush.Style := bsClear;
Font.Color := clYellow;
Font.Size := 12;
Textout(15, 15, 'FPS: '+inttostr(DXTimer1.FrameRate));
Textout(15, 29, 'Tutorial 1: Inicializando');
finally
Release; { Indispensable }
end;
end;
// Sacamos la escena a pantalla
DXDraw.Flip;
end;
Aunque nuestra escena sólo pone un
fondo azúl y un texto en nuestra vista, el código tiene varias cosas que
comentar.
Empezaremos por el principio (¿lógico, no? 8D): la
primera línea.
If not DXDraw.CanDraw Then
Exit;
Esta línea de código nos asegura que
DXDraw está preparado para renderizar una nueva escena. Es común a toda
aplicación DelphiX ya sea 3D ó 2D y prescindir de ella es garantía de
fracaso total 8D
Tras esta primera línea tenemos el código que
renderiza nuestra sencilla escena cambiadora de color.
Antes de crear la escena, preparamos la vista borrando
su contenido, lo que hacemos con una llamada a la función Clear de
la propiedad D3DDevice7 de DXDraw. Esta función requiere
los siguientes parámetros:
dwCount : Word -> él número de vistas a
"limpiar". Dado que sólo hemos definido 1 vista este valor es
1. También sería 1 si teniendo mas de 1 vista sólo queremos borrar el
contenido de una de ellas.
lpRects : TD3DRect -> dimensiones de la(s) vista(s) a
"limpiar". Este valor puede ser el puntero del buffer en el
que tendremos todas las estructuras TD3DRect con las dimensiones de las
vistas que queremos limpiar si dwCount >1
dwFlags : DWord -> flag que regula el comportamiento de la
función. Este flag puede ser:
D3DCLEAR_TARGET -> limpia la vista seleccionada
D3DCLEAR_ZBUFFER -> limpia el Z-Buffer (si existe)
D3DCLEAR_STENCIL -> limpia el buffer de pixels (stencil buffer)
o una combinación de los 3 mediante el operador lógico Or.
dwColor : TD3DColor -> color usado para
"limpiar" la(s) vista(s). Este color también se usa para
limpiar el Z-Buffer (si existe)
dvZ : TD3DValue -> Nivel al que se "limpiará" el
Z-Buffer. Este es un valor en el rango 0.0-1.0; para 0.0 se borra lo que
esté en el primer plano, borrándose todo el buffer hasta su máxima
profundidad para el valor 1.0.
dwStencil : DWord -> valor al que se modificarán los píxels
del Buffer de pixels (Stencil Buffer) si existe.
Una vez explicados los parámetros que necesita la
función Clear creo que no hace falta explicar el porqué la hemos
incluído en un IF-THEN-ELSE que controla si tenemos activo, o no,
el Z-Buffer.
Para fijar el color que usaremos para
"limpiar" la vista utilizamos la función RGB_MAKE que
devuelve un valor de tipo TD3DColor y que requiere los valores Red,
Green, Blue (rojo, verde, azul) para componer un color en
formato RGB. Sugiero al lector que experimente con estos valores teniendo
en cuenta que han de estar en el rango 0-255.
Los 2 últimos parámetros son ignorados o no
dependiendo de la presencia de los flags D3DCLEAR_ZBUFFER y D3DCLEAR_STENCIL.
Si no se pasan estos flags a la función, dichos parámetros son
ignorados, sin embargo conviene darles un valor cualquiera para evitar
errores de compilación (1.0, y 0, son valores seguros que no darán
excesivos problemas si hay un error en la declaración de flags), pero si
se incluyen los correspondientes flags hay que tener claros los valores
que se pasen pues serán usados por la función.
Antes de continuar con la explicación del código voy
a dar un poco de teoría (si está familiarizado con el page flipping
puede saltarse el párrafo siguiente)
La función Clear no borra la vista tal cual, es
decir, sin el código que sigue a la llamada a esta función, veríamos
que la ventana no toma el color azul que queremos que tenga. Esto es
debido a que lo que se borra no es la memoria de video "visible"
o, por asi decirlo, de pantalla, sinó la memoria auxililar denominada
BackBuffer. De hecho, la escena se renderiza sobre esta memoria auxiliar y
al final del bucle de renderizado se vuelca a la memoria de pantalla para
hacerla visible. A esta técnica se la denomina page flipping
(intercambio de página). Dicho esto, volvamos al comentario del código.
Lo siguiente que encontramos es una llamada a DXDraw.D3DDevice7.BeginScene
seguida de una llamada a DXDraw.D3DDevice7.EndScene. Dado que
nuestra aplicación sólo cambia de color nuestra vista y pone un sencillo
texto en ella (lo cual comentaremos mas adelante) es lógico que entre
estas 2 funciones no haya código alguno pues nada hay que renderizar,
pero como ya habrá supuesto usted, aquí va el código que renderiza una
escena 3D (p.ej. un cubo rotando sobre un eje).
Con BeginScene notificamos al sistema que vamos
a renderizar una escena 3D y nos asegura que podemos proceder.
Con EndScene notificamos que hemos acabado la
renderización y nos habilita para renderizar otra escena.
Se puede encadenar el renderizado de varias escenas una
detrás de otra siempre que las incluyamos entre los correspondientes BeginScene
y EndScene, pero NO SE PUEDE INCLUIR EL RENDERIZADO DE UNA ESCENA
DENTRO DE EL RENDERIZADO DE OTRA; es decir, un código como el siguiente:
DXDraw.D3DDevice7.BeginScene;
......
DXDraw.D3DDevice7.BeginScene;
.....
DXDraw.D3DDevice7.EndScene;
.....
DXDraw.D3DDevice7.EndScene;
sólo es garantía de problemas.
Con nuestra escena ya renderizada, sólo falta poner el
texto en pantalla y hacer el intercambio de página (page flipping).
Para lo primero utilizamos el Canvas que
incorpora el componente DXDraw. A todos los efecto el Canvas
de DXDraw funciona exactamente igual que el que encontramos en
cualquier otro componente Delphi que tenga la propiedad Canvas. La
única diferencia es que hemos de encapsularlo en un Try-Finally-end
poniendo en la sección Finally una llamada al método DXDraw.Surface.Canvas.Release
para garantizar que todo irá como la seda :)) No le recomiendo que
experimente con esta línea, le garantizo que los efectos de su ausencia
son desastrosos, no sólo para la aplicación.
Del intercambio de página (page flipping) se
encarga el método DXDraw.Flip que ha de ser siempre la última
instrucción de nuestro bucle de renderizado. Esta instrucción es la que
se encarga de volcar todos los cambios hechos en el BackBuffer a nuestra
vista.
Con esto finalizamos este primer tutorial.
En este primer tutorial hemos prescindido de todo código
que verifique errores y asegure el correcto funcionamiento de la aplicación,
dado que no era el objeto de este tutorial, sin embargo, en posteriores
ejemplos iremos añadiendo código que monitorice el funcionamiento de la
aplicación y genere el correspondiente mensaje de error si se produce
alguno. En el siguiente tutorial aprenderemos cómo determinar las
capacidades DirectX del hardware, los modos de video disponibles, y a
seleccionar el que mas nos guste para nuestra aplicación.
Por último recomendaros que tecleéis el código pues
ello os ayudará a entender mejor lo explicado en este tutorial.
Descargar el Código Fuente de Ejemplo
|