{{notification.text}}

MirGames

Аннотация

Почти все программисты желающие создать игру, в наше время, рано или поздно встают перед выбором графического апи, OpenGL или Direct3D.

Не вдаваясь превосходства и недостатки того или другого стоит заметить, что именно Microsoft сейчас находится на вершине многих рынков программ и в добавок их самое огромное дитя - Windows стоит у большинства из всех пользователе персональных компьютеров. Исходя из этого можно предположить что у DirectX достаточно надёжное будущее.

В этой статье не будет подниматься спор о том какой из апи лучше в плане скорости, т.к. оба имеют недостатки и преимущества, кроме того любой программист хорошо владеющий Direct3D или OpenGL сможет обойти их недостатки и добиться нужной скорости.

В этой статье вы познакомитесь с Direct3D, научитесь создавать 2д и 3д сцены, работать с текстурами, материалами и освещением, а так же пользоваться некоторыми "продвинутыми" спец эффектами. Стоит сразу заметить, что код приведённый здесь - написан на Delphi, поэтому статьи предполагают что вы уже имеете Delphi компилятор и хорошо владеете этим языком.

Подготовка приложения

1. Direct3D Headers

Прежде всего стоит указать несколько мест где можно найти много полезной информации по Direct3D:

  • www.clootie.ru - сайт распространяющий заголовки Direct3D для Delphi, необходимые для компиляции.
  • msdn.microsoft.com - одна из самых больших баз информации по программированию под Windows.
  • mirgames.ru - сайт для которого собственно и была написана эта статья, задавайте свои вопросы на форуме, на них обязательно ответят.

Итак, для того чтобы подготовить Delphi для работы с Direct3D вам потребуются заголовки Direct3D для Delphi и библиотека D3DX (D3DX9_2x.dll). Найти их можно на сайте www.clootie.ru.

После того как вы скачали и распаковали заголовки, нужно прописать их в библиотеку Delphi.

D3DX9_2x.dll нужно добавить в папку WINDOWSSystem кроме того её нужно распространять вместе с программами написанными с D3D (не все программы с D3D требуют использования D3DX библиотеки, однако она очень сильно облегчает работу и поэтому настоятельно рекомендуется).

Теперь компилятор Delphi готов к работе с D3D.

2. Инициализация

Для начала работы с D3D нужно добавить модуль Direct3D9 в uses. Затем нужно создать два основных объекта в D3D - g_D3DObject: IDirect3D9 и g_Device: IDirect3DDevice9. Во время инициализации так же нужно создать переменную для хранения всех параметров инициализации - l_D3DPresentParameters: TD3DPresentParameters. Далее сам код инициализации:

function TForm1.Initialize: HRESULT;
var
   l_D3DPresentParameters: TD3DPresentParameters;
begin
   //Данная процедура считается не успешно выполненной если
   //она не дошла до конца.
   Result:= E_FAIL;

   //Создание главного объекта D3D
   g_D3DObject := Direct3DCreate9(D3D_SDK_VERSION);
   //Если по какой-то причине D3D объект не был создан, то прекращаем работу
   if (g_D3DObject = nil) then Exit;

   //Очищаем переменную с параметрами
   FillChar(l_D3DPresentParameters, SizeOf(l_D3DPresentParameters), 0);

   //Настраиваем оконный режим 
   //(для инициализации в полноэкранном режиме не достаточно смены параметра Windowed на false)
   l_D3DPresentParameters.Windowed := true;
   //Этот параметр влияет на вертикальную синхронизацию в полноэкранном режиме,
   //в оконном режиме для включения вертикальной синхронизации, нужно поставить
   //этот параметр на D3DPRESENT_INTERVAL_ONE и изменить SwapEffect на D3DSWAPEFFECT_COPY.
   //D3DPRESENT_INTERVAL_IMMEDIATE - выключение вертикальной синхронизации
   //D3DPRESENT_INTERVAL_ONE - включение вертикальной синхронизации
   l_D3DPresentParameters.PresentationInterval := D3DPRESENT_INTERVAL_IMMEDIATE;

   l_D3DPresentParameters.MultiSampleType := D3DMULTISAMPLE_NONE;
   l_D3DPresentParameters.EnableAutoDepthStencil := true;


   //формат Z и Stencil буфера (D3DFMT_D16 - 16 битный z буфер, stencil буфер выключен)
   l_D3DPresentParameters.AutoDepthStencilFormat := D3DFMT_D16;

   //В большинстве случаев SwapEffect должен быть установлен на D3DSWAPEFFECT_DISCARD
   //Если в оконном режиме требуется вертикальная синхронизация, то этот
   //параметр должен быть D3DSWAPEFFECT_COPY
   l_D3DPresentParameters.SwapEffect := D3DSWAPEFFECT_DISCARD;
   l_D3DPresentParameters.Flags:=D3DPRESENTFLAG_LOCKABLE_BACKBUFFER;
   //BackBuffer есть поверхность на которую будет отображаться вся графика,
   //следовательно здесь устанавливаются её параметры.
   //в оконном режиме BackBufferFormat должен всегда быть D3DFMT_UNKNOWN,
   //т.к. D3D придётся работать с тем форматом который уже стоит в системе.
   l_D3DPresentParameters.BackBufferCount := 1;
   l_D3DPresentParameters.BackBufferWidth := Width;
   l_D3DPresentParameters.BackBufferHeight := Height;
   l_D3DPresentParameters.BackBufferFormat := D3DFMT_UNKNOWN;

   //После того как все параметры установлены можно создавать g_Device
   //почти все дальнейшие действия будут выполняться через g_Device 
   //а не через g_D3DObject
   Result := g_D3DObject.CreateDevice(
     D3DADAPTER_DEFAULT,
     D3DDEVTYPE_HAL,
     Handle,
     D3DCREATE_SOFTWARE_VERTEXPROCESSING,
     @l_D3DPresentParameters,
     g_Device
   );

   //Если по какой-то причине g_Device не был создан, 
   //то прекращаем выполнение функции, при этом нужно
   //освободить все ранее созданные объекты 
   if FAILED(Result) then
   begin
     g_D3DObject := nil;
     Exit;
   end;

   //Если процедура дошла до конца, то она считается успешно выполненной
   Result := S_OK;
end;

Если инициализация прошла успешно, то можно начать рисовать, для этого нужно сначала запустить таймер, например обычный системный таймер сгодится, однако в этом примере показано как просто прописать процедуру в Application.OnIdle, чтобы она выполнялась как таймер:

procedure TForm1.EnableTimer;
begin
   //В данном случае Timer является процедурой в которой происходит
   //отрисовка сцены.
   //процедура Timer должна иметь следующий формат
   //procedure Timer(Sender: TObject; var Done: Boolean);
   Application.OnIdle := Timer;
end;

Далее рассмотрим саму отрисовку:

procedure TForm1.Timer(Sender: TObject; var Done: Boolean);
begin
   //переменная Done должна быть false для того чтобы таймер продолжал работать
   Done := false;
   //функция Clear очищает поверхность отрисовки (D3DCLEAR_TARGET), кроме того она
   //может очищать Z (D3DCLEAR_ZBUFFER) и Stencil (D3DCLEAR_STENCIL) буферы.
   //В данном примере Z буфер выключен и код очистки Z буфера приведён
   //для демонстрации
   g_Device.Clear(0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER, $ff8080ff, 1.0, 0);
   g_Device.BeginScene;

   //Здесь происходит отрисовка всей сцены.

   g_Device.EndScene;
   g_Device.Present(nil, nil, 0, nil);
end;

И последнее но далеко не мало важное - это освобождение ресурсов D3D после завершения работы. Для того чтобы избежать проблем с любыми com объектами (все Direct3D интерфейсы являются com объектами) нужно их освобождать в порядке, обратном их созданию. Например, g_D3DObject был создан до g_Device, значит g_D3DObject должен быть освобождён после освобождения g_Device. Для освобождения интерфейсов в Delphi достаточно присвоить им значение nil.

procedure TForm1.WrapUp;
begin
   if g_Device <> nil then g_Device := nil;
   if g_D3DObject <> nil then g_D3DObject := nil;
end;

3. Инициализация в полноэкранном режиме

Инициализация в полноэкранном режиме не много отличается от инициализации в оконном режиме. Для этого нужно изменить несколько параметров инициализации.

  • l_D3DPresentParameters.Windowed := false;
  • l_D3DPresentParameters.BackBufferFormat := D3DFMT_A8R8G8B8;

Можно проверить поддержку того или иного формата, до инициализации, через g_D3DObject, в данном случае используется формат D3DFMT_A8R8G8B8, который поддерживается большинством видео карт.

Вывод текста с ID3DXFont

В D3D существует много различных способов вывода текста, один из них это интерфейс ID3DXFont. Если вы ещё не скопировали D3DX библиотеку в WINDOWS/System, то сейчас самое время. Для начала нужно добавить юнит D3DX9 в uses и создать переменную g_Font: ID3DXFont. Далее его нужно инициализировать:

function TForm1.CreateFont(FontName: string; Size: cardinal): HRESULT;
begin
   Result := E_FAIL;

   //Здесь можно указать множество параметров шрифта
   //после выполнения этой функции g_Font должен стать
   //рабочим объектом.
   Result := D3DXCreateFont(
     g_Device,
     Size,
     0,
     FW_NORMAL,
     1,
     false,
     DEFAULT_CHARSET,
     OUT_DEFAULT_PRECIS,
     ANTIALIASED_QUALITY,
     DEFAULT_PITCH or FF_DONTCARE,
     PChar(FontName),
     g_Font
   );

   //Если же по какой-то причине шрифт не был создан, то выходим
   //из функции, возвращая сообщение о не успешном завершении.
   if FAILED(Result) then exit;

   Result := S_OK;
end;

После инициализации шрифта можно начинать печатать:

procedure TForm1.Print(x,y: integer; Text: string; Color: DWORD);
var
   TextRect: TRect;
begin
   //Сначала нужно задать позицию текста в виде TRect
   TextRect:=Rect(x,y,x,y);

   //Затем можно печатать текст
   g_Font.DrawTextA(
     nil,
     PChar(Text),
     -1,
     @TextRect,
     DT_LEFT or DT_NOCLIP,
     color
   );
end;

Нужно не забыть добавить освобождение g_Font в процедуру очистки. У ID3DXFont есть, однако, один недостаток - для того чтобы изменить размер шрифта нужно его заново инициализировать.

Отрисовка геометрии

В D3D любая геометрия отличная от точек и линий есть треугольник! Поэтому, например, квадрат можно построить из двух треугольников и т.д. Любой треугольник в свою очередь состоит из трёх точек (вертексов). Работа с вертексами в D3D обычно осуществляется через вертексный буфер (IDirect3DVertexBuffer9).

Начнём с простого: вывод 2д геометрии. Для начала нужно объявить наш вертексный буфер:

var
   Form1: TForm1;
   g_D3DObject: IDirect3D9;
   g_Device: IDirect3DDevice9;
   g_VB: IDirect3DVertexBuffer9; //это и будет вертексный буфер.

Далее очень важно объявить формат вертексов, что бы Direct3D знал как их читать. Сами вертексы могут хранить огромное колличество значений, таких как положение в пространстве (D3DFVF_XYZ), цвет (D3DFVF_DIFFUSE), текстурные координаты (D3DFVF_TEX1, D3DFVF_TEX2... D3DFVF_TEX8), нормали (D3DFVF_NORMAL) и много ещё чего...

Самое главное, если бы вертексы всё время хранили всю возможную информацию, то их размер был бы очень большой, поэтому передача таких вертексов из оперативной памяти в видио память была бы очень медленной. Именно поэтому в вертексах нужно хранить только ту информацию которая нужна для определённой задачи (если мы пишем простую 2д игру то нам в большинстве случаев просто бессмысленно хранить нормали вертексов).

Передаются вертексы в видео память большими "пачками", когда по 100 вертексов когда по 100,000 вертексов в одном куске памяти. D3D придётся эту память разбить на отдельные вертексы и для того чтобы он мог опознать каждый отдельный вертекс он должен знать их формат.

И так объявляем формат наших 2д вертексов:

  TVertex = record
     x,y,z,rhw: single;
     color: DWORD;
   end;
 ...
const
   FVF = D3DFVF_XYZRHW or D3DFVF_DIFFUSE;
//D3DFVF_XYZRHW значит что вертексы не будут проходить трансформацию
//            (проще говоря это формат для 2д вертексов).
//D3DFVF_DIFFUSE значит что вертексы будут хранить цвет.

Вот мы и закончили объявлять вертексы.

Теперь нужно создать вертексный буфер:

function TForm1.CreateVertexBuffer: HRESULT;
begin
   Result := g_Device.CreateVertexBuffer(
     sizeof(TVertex)*4,  //Размер буфера
     0,                  //Параметры использования буфера
     FVF,                //Формат вертексов
     D3DPOOL_MANAGED,    //(коротко: где именно будут храниться вертексы)
     g_VB,               //Наш указатель на буфер
     nil                 //Просто nil
   );
end;

Теперь мы будем работать с вертексным буфером через g_VB.

Последнее что нужно сделать перед тем как начать что-либо рисовать это загрузить вертексы в вертексный буфер:

function TForm1.LoadVertices: HRESULT;
var
   Vertices: array [0..3] of TVertex;
   pVertices: Pointer;
begin
   //т.к. мы рисуем прямоугольник то нам нужно всего 4 вертекса
   //каждому из них ставим свой цвет
   //Первый вертекс:
   Vertices[0].x:=10;  Vertices[0].y:=10;  Vertices[0].z:=0; Vertices[0].rhw:=1;
   Vertices[0].color:=$ffff0000;
   //Второй вертекс:
   Vertices[1].x:=790; Vertices[1].y:=10;  Vertices[1].z:=0; Vertices[1].rhw:=1; 
   Vertices[1].color:=$ff00ff00;
   //Третий:
   Vertices[2].x:=10;  Vertices[2].y:=590; Vertices[2].z:=0; Vertices[2].rhw:=1; 
   Vertices[2].color:=$ff0000ff;
   //И четвёртый:
   Vertices[3].x:=790; Vertices[3].y:=590; Vertices[3].z:=0; Vertices[3].rhw:=1; 
   Vertices[3].color:=$ffffff00;

   //и последнее - скопировать массив вертексов в вертексный буфер
   //в обычном состоянии мы не можем просто взять и скопировать вертексы
   //т.к. вертексный буфер может использоваться видео картой
   //поэтому до того как копировать вертексы нам нужно запереть буфер
   //(получить к нему эксклюзивный доступ)
   //для этого вызываем g_VB.Lock
   //далее всё просто g_VB.Lock возвращает указатель на память
   //в которую мы и копируем наши вертексы
   //после чего естественно "отпираем" буфер (вызываем g_VB.Unlock)
   Result := g_VB.Lock(0, sizeof(TVertex)*4, pVertices, 0);
   if FAILED(Result) then Exit;
   Move(Vertices[0], pVertices^, sizeof(TVertex)*4);
   g_VB.Unlock;
end;

Итак, теперь можно начинать рисовать. Рисование, естественно, происходит в таймере:

procedure TForm1.Timer(Sender: TObject; var Done: Boolean);
begin
   Done := false;
   g_Device.Clear(0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER, $ff8080ff, 1.0, 0);
   g_Device.BeginScene;

   //Ставим источник вертексов (наш вертексный буфер)
   g_Device.SetStreamSource(0, g_VB, 0, sizeof(TVertex));
   //Ставим формат вертексов
   g_Device.SetFVF(FVF);
   //И наконец выполняем отрисовку
   //(подробнее эта функция будет рассмотрена позже)
   g_Device.DrawPrimitive(
     D3DPT_TRIANGLESTRIP, 0, 2
   );

   g_Device.EndScene;
   g_Device.Present(nil, nil, 0, nil);
end;

Во время завершения программы g_VB нужно "отпустить" как и другие интерфейсы.

Вот что должно получиться:

Текстуры

Если вы чувствуете себя не уверенно с тем как работают вертексы, то это нормально... После этой главы я настоятельно рекомендую поэкспериментировать с примерами и может даже написать свою небольшую 2д игру, что-нибудь в роде арканоида. Это поможет вам освоиться.

Итак... текстуры, работать с текстурами в D3D очень легко, однако есть в этом деле и несколько осложнений которые сразу не очевидны.

Первое - нужно всегда помнить что размер текстуры должен быть равен степени двойки (включая нулевую степень). Напимер, текстура может иметь размеры 256х256, 64х512, 1х32 и т.д.

Второе - все видео карты на данный момент имеют ограничения размера текстур, самое распространённое - 2048х2048, поэтому использование больших текстур не рекомендуется (я бы порекомендовал не превышать 1024х1024). Это основные ограничения текстур в D3D (собственно как и в OpenGL). Далее перейдём к практике...

Для начала, чтобы использовать текстуру, её нужно загрузить. Текстура в D3D как и многое другое, это интерфейс IDirect3DTexture9. Библиотека D3DX предоставляет несколько функций для загрузки текстур, все они будут рассмотрены позже, пока мы воспользуемся лишь одной из них D3DXCreateTextureFromFile.

Первым делом создаём глобальную переменную:

var
   g_Texture: IDirect3DTexture9;</code>
Далее загружаем в неё текстуру:

<code>function TForm1.LoadTexture: HRESULT;
begin
   Result := D3DXCreateTextureFromFile(
     g_Device,
     'box.jpg', //Имя файла текстуры
     g_Texture  //Переменная интерфейса текстуры
   );
end;

Теперь нужно изменить формат вертексов чтобы в них была информация о текстурных координатах.

Текстурные координаты указывают как текстура будет накладываться на геометрию, они измеряются от 0 до 1. Например UV = 0,0 - левый верхний пиксель текстуры, UV = 1,1 - правый нижний пиксель текстуры, UV = 1,0 - правый верхний пиксель текстуры.

const
   FVF = D3DFVF_XYZRHW or D3DFVF_TEX1;

Структура вертексов:

TVertex = record
   x,y,z,rhw: single;
   tu,tv: single; //Текстурные координаты
end;

При заполнении информации вертексов в буфере, нужно указать текстурные координаты:

function TForm1.LoadVertices: HRESULT;
var
   Vertices: array [0..3] of TVertex;
   pVertices: Pointer;
begin
   //Vertex 0
   Vertices[0].x := 200; Vertices[0].y := 100; Vertices[0].z := 0; Vertices[0].rhw := 1;
   Vertices[0].tu := 0; Vertices[0].tv := 0;  //Левый верхний угол
   //Vertex 1
   Vertices[1].x := 600; Vertices[1].y := 100; Vertices[1].z := 0; Vertices[1].rhw := 1;
   Vertices[1].tu := 1; Vertices[1].tv := 0;  //Правый верхний угол
   //Vertex 2
   Vertices[2].x := 200; Vertices[2].y := 500; Vertices[2].z := 0; Vertices[2].rhw := 1;
   Vertices[2].tu := 0; Vertices[2].tv := 1;  //Левый нижний угол
   //Vertex 3
   Vertices[3].x := 600; Vertices[3].y := 500; Vertices[3].z := 0; Vertices[3].rhw := 1;
   Vertices[3].tu := 1; Vertices[3].tv := 1;  //Правый нижний угол

   Result := g_VB.Lock(0, sizeof(TVertex)*4, pVertices, 0);
   if FAILED(Result) then Exit;
   Move(Vertices[0], pVertices^, sizeof(TVertex)*4);
   g_VB.Unlock;
end;

И последнее что осталось сделать - это установить текстуру перед отрисовкой.

procedure TForm1.Timer(Sender: TObject; var Done: Boolean);
begin
   Done := false;
   g_Device.Clear(0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER, $ff8080ff, 1.0, 0);
   g_Device.BeginScene;

   g_Device.SetTexture(0, g_Texture);  //Здесь текстура устанавливается в нулевой texture stage
   g_Device.SetStreamSource(0, g_VB, 0, sizeof(TVertex));
   g_Device.SetFVF(FVF);
   g_Device.DrawPrimitive(
     D3DPT_TRIANGLESTRIP, 0, 2
   );

   g_Device.EndScene;
   g_Device.Present(nil, nil, 0, nil);
end;

Вот и всё!

1. Отрисовка текстуры с альфа каналом

Библиотека D3DX может загружать много распространённых форматов, BMP, JPG, PNG, TGA и т.д. некоторые форматы могут содержать альфа канал (прозрачность), этот альфа канал будет загружен в текстуру. Для отрисовки текстуры с учётом альфа канала, нужно добавить несколько строчек в код.

Загрузка текстуры из PNG файла:

function TForm1.LoadTexture: HRESULT;
begin
   Result := D3DXCreateTextureFromFile(
     g_Device,
     'DJX.png',
     g_Texture
   );
end;

Перед отрисовкой нужно включить режим alpha blend:

g_Device.SetRenderState(D3DRS_ALPHABLENDENABLE, ITrue);
g_Device.SetRenderState(D3DRS_SRCBLEND,D3DBLEND_SRCALPHA);
g_Device.SetRenderState(D3DRS_DESTBLEND,D3DBLEND_INVSRCALPHA);

Это всё что нужно сделать для вывода текстур с альфа каналом.

Вы наверное заметили что когда картинку увеличить во много раз больше её оригинального размера, то можно увидеть квадратики, пиксели. Как правило, это выглядит не очень красиво и для того чтобы сгладить резкие переходы между пикселями в D3D используется Фильтрация текстуры. Её нужно включать перед отрисовкой.

g_Device.SetSamplerState(0,D3DSAMP_MAGFILTER,D3DTEXF_LINEAR);
g_Device.SetSamplerState(0,D3DSAMP_MINFILTER,D3DTEXF_LINEAR);

Теперь вы знаете все необходимые детали для написания своей 2д игры!

01.12.06 22:21