Войти
ПрограммированиеСтатьиГрафика

DirectX 8: Начинаем работу с DirectX Graphics.

Автор:

(пример написан на основе первого "родного" туториала к DirectX SDK 8.0)

Ну, наконец-то мы дошли до самой сути, а именно — до начала работы непосредственно с DirectX! Сейчас напишем вместе прогу, которая создаст класс, окошко, инициализирует объекты Direct3D и ... в итоге у нас получится - сами увидите что :-) Советую СКАЧАТЬ ПРИМЕР в архиве и читать дальше, имея исходники перед глазами. Чтобы открыть пример, необходимо сначала разархивировать его в отдельную директорию, затем нажать File->Open Workspace и открыть файл "D3D Init.dsw" из этой директории. Ну... поехали.

Сначала необходимо написать include'ы, описать глобальные переменные, которые мы будем использовать в программе, а также объявить прототипы функций:

//Включаем все функции, необходимые для работы с D3D. 
//<windows.h> уже включен в этом файле
#include <d3d8.h>

#include <stdio.h> //В дальнейшем нам понадобится функция sprintf()
LPDIRECT3D8 g_pD3D = NULL; //Понадобится нам, чтобы создать D3DDevice

 //Это наше устройство rendering'а (Rendering Device)
LPDIRECT3DDEVICE8  g_pd3dDevice = NULL;
WNDCLASSEX wclass;
//Объявляем прототипы функций
VOID Init(HWND);  //Инициализация D3D
VOID Render();  //Рендеринг сцены
VOID Sweep();  //Очистка после выполнения программы
//Обработка сообщений, поступивших окну
LRESULT CALLBACK MessageProc(HWND,UINT,WPARAM,LPARAM);

Что же такое Render? "Render" с английского переводится как: "переводить", "просчитывать", "визуализировать". Все объекты, которые находятся на нашей виртуальной сцене, хранятся в памяти в виде отдельных блоков: источники света, каркас, состоящий из вершин (точка в 3-D пространстве), текстуры, и т.д. Но видеокарта не может все это показать на экране, т.к. может вывести только последовательность разноцветных точек. Rendering Device как раз и выполняет эту функцию, т.е. преобразует всю нашу сцену в "понятный" для видеокарты вид.

В своих статьях я не буду переводить некоторые английские термины на русский язык, а буду их "руссифицировать" (например, я не буду переводить "rendering device", как "делательное устройство" или "устройство просчёта", а буду просто говорить "устройство рендеринга" или вообще "девайс для рендеринга" (всякое может случиться %) )). Дело не в том, что я не могу подобрать тому или иному английскому термину русский эквивалент. Просто иногда лучше пользоваться "родными" названиями (мы же не называем "Visual C" Визульным Си =-)). Едем дальше...

В нашей программе будет пять функций: функция WinMain(), Init(), Render(), Sweep() и функция обработки сообщений MessageProc(). Каждая функция представляет собой некий блок программы, который выполняет исключительно свою операцию. Например Init() будет инициализировать Direct3D (в дальнейшем, просто D3D), Render() - рендерить сцену, а Sweep() - производить очистку после выполнения программы. Я постарался, как можно больше упростить программу, поэтому урезал некоторые моменты (например, обработку ошибок, которые могут возникнуть при инициализации). В последующих программах мы будем стараться "честно" учитывать все возможные ошибки и обрабатывать их. Итак, рассмотрим подробнее, что же делают наши функции.

Функция WinMain().

Самая главная функция. Именно с нее начинается выполнение любой программы, написанной под Windows. В нашем случае она делает вот что:

Описываем переменные.

HWND hWnd;  //Handle окна
MSG msg;

Затем идет настройка параметров класса, регистрация класса в системе и создание окна, представляющего этот класс.

  //Настраиваем параметры класса wclass перед его регистрацией в системе
  wclass.cbSize=sizeof(WNDCLASSEX);
  wclass.style=CS_DBLCLKS;
  wclass.lpfnWndProc=&MessageProc; //Функция обрабатывающая сообщения
  wclass.cbClsExtra=0;
  wclass.cbWndExtra=0;
  wclass.hInstance=hInst;
  wclass.hIcon=LoadIcon(NULL,IDI_APPLICATION);
  wclass.hCursor=LoadCursor(NULL,IDC_ARROW);
  wclass.hbrBackground=GetSysColorBrush(COLOR_APPWORKSPACE);
  wclass.lpszMenuName=NULL;
  wclass.lpszClassName="InitClass";
  wclass.hIconSm=NULL;

  //Выходим, если не удалось зарегистрировать класс
  if (!RegisterClassEx(&wclass)) return 0;
  //Теперь создадим окно и занесем его handle в hWnd
  hWnd=CreateWindow( "InitClass","D3D Init",
                     WS_SYSMENU|WS_OVERLAPPED|WS_MINIMIZEBOX,
                     100,100,420,300,NULL,NULL,hInst,NULL);

Подробное описание структуры WNDCLASSEX и параметров функции CreateWindow() смотри в статье JM'а "Введение в программирование под Windows". Стоит обратить внимание на то, что мы после всех манипуляций будем иметь в hWnd дескриптор окна, в котором затем сможем отображать нашу сцену.

Теперь обратимся к функции инициализации D3D:

Init(hWnd);

Остается только показать на экране наше окно

  ShowWindow(hWnd,nCmdShow); //Рисуем окошко
  UpdateWindow(hWnd);

и организовать цикл обработки сообщений окну

  while (GetMessage(&msg,NULL,0,0)) //Цикл обработки сообщений
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  return (msg.wParam);

Функция Init().

Теперь, когда у нас есть окно, мы готовы к инициализации объекта Direct3D. Зачем он нужен? Как только будет создан объект Direct3D, мы сможем использовать метод IDirect3D8::CreateDevice() для создания устройства Direct3D (предварительно настроив его параметры), т.е. нашего устройства рендеринга. Параметры устройства задаются непосредственно перед его созданием с помощью структуры D3DPRESENT_PARAMETERS, и передаются в метод IDirect3D8::CreateDevice(). Рассмотрим этот процесс подробнее. Итак, создаем объект Direct3D:

g_pD3D=Direct3DCreate8(D3D_SDK_VERSION);

Единственным параметром функции Direct3DCreate8 является UINT SDKVersion, который должен быть D3D_SDK_VERSION. Это необходимо для того, чтобы при компиляции программы были использованы правильные заголовочные файлы [*.h] (замечание: если после создания объекта были подключены новые адаптеры, необходимо повторно инициализировать объект Direct3D, чтобы использовать их). Если в процессе инициализации произошла ошибка, то Direct3DCreate8 возвращает NULL (т.е. пустой указатель).

Уже пора определиться, что же будет делать наша программа :).

После запуска она создаст и прорисует окошечко, в заголовке которого будет показано текущее разрешение графического адаптера, а в рабочей области - отрендеренная сцена. В данном примере сцена не будет содержать никаких объектов, поэтому отрендерится только темно-синий фон.

Заголовок окна ("window name" стоило перевести как "имя окна", но тогда как-то несолидно звучит :) ) будет содержаться в строке символов str (т.е. str указывает на первый символ строки). Чтобы сформировать str, необходимо узнать ширину (Width) и высоту (Height) текущего видеорежима, а затем воспользоваться стандартной Си'шной функцией sprintf(), которая осуществляет форматированный вывод в строку. Как же узнать ширину и высоту текущего видеорежима? Для этого воспользуемся методом IDirect3D8::GetAdapterDisplayMode, который возвращает текущий режим видеоадаптера в структуру D3DDISPLAYMODE. Затем из структуры "выуживаем" необходимые параметры Width и Height, формируем строку str. Остается только изменить заголовок нашего окна с помощью стандартной WinAPI функции SetWindowText():

  D3DDISPLAYMODE dm;  //В переменной dm будет храниться текущий видеорежим
  //Заголовок нашего окна. Будет генериться в зависимости от текущего видеорежима
  char str[128];
  g_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT,&dm);
  Width=dm.Width;
  Height=dm.Height;
  sprintf(str,"(D3D Init) Current adapter mode: [%dx%d]",Width,Height);
  SetWindowText(hWnd,str);

Наконец, займемся непосредственно инициализацией D3D. Как было сказано выше, нам понадобится переменная-структура типа D3DPRESENT_PARAMETERS. Опишем эту переменную и выделим для нее память:

  //Структура понадобится для создания D3DDevice
  D3DPRESENT_PARAMETERS p_p;
  ZeroMemory(&p_p, sizeof(p_p));

Меняя поля структуры p_p, мы можем гибко настраивать параметры нашего трехмерного приложения. На этой стадии определяется, будет ли наше приложение оконным или полноэкранным, задается частота обновления экрана (только для полноэкранного режима), необходимое количество бэк-буферов (BackBuffer) и т.д. Бэк-буфер - это область памяти для хранения одного неактивного в данный момент (т.е. визуально скрытого от пользователя) видеоэкрана. В бэк-буфер можно рендерить сцену, в то время, как пользователь видит на экране другую картинку, и практически моментально выводить содержимое бэк-буфера на экран.

Мы создаем оконное приложение (об этом говорит строка p_p.Windowed=TRUE;), поэтому необходимо установить формат бэк-буфера таким же, как и формат текущего видеорежима (как ты помнишь, он у нас хранится в переменной dm.Format). Поле SwapEffect задает способ обмена между франт-буфером (FrontBuffer, т.е. тот, который сейчас активен) и бэк-буфером. Существуют несколько значений этого параметра. Мы будем использовать D3DSWAPEFFECT_DISCARD ("discard" переводится с английского, как "сбрасывать", "отбрасывать"), т.е. после вывода на экран содержимое бэк-буфера заполняется "шумами" ("noise"). Реализуем задумки в программный код:

p_p.BackBufferFormat = dm.Format;
p_p.Windowed=TRUE;
p_p.SwapEffect=D3DSWAPEFFECT_DISCARD;

Последний шаг, который осуществляет наша функция Init() - создание устройства. Для этого воспользуемся методом IDirect3D8::CreateDevice().

Укажем следующие параметры:

a. D3DADAPTER_DEFAULT - используется стандартный видеоадаптер
b. D3DDEVTYPE_REF - визуализация будет происходить исключительно программными средствами Direct3D
c. hWnd - собственно, идентификатор окна
d. D3DCREATE_SOFTWARE_VERTEXPROCESSING - обработка точек будет происходить исключительно программными средствами
e. &p_p - указатель на структуру, описывающую параметры создающегося устройства
f. &g_pd3dDevice - адрес указателя на интерфейс IDirect3DDevice8, который будет создан
Вот, что должно получиться:

  g_pD3D->CreateDevice( D3DADAPTER_DEFAULT,D3DDEVTYPE_REF,hWnd,
                        D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                        &p_p,&g_pd3dDevice);

Функция Render()

Эта функция как раз рендерит сцену. Принцип ее действия:

a. Очищается бэк-буфер, и в него рендерится сцена (итого, в бэк-буфере окажется темно-синий экран, т.к. сцена не содержит объектов)
b. Происходит передача данных во франт-буфер
c. Содержимое франт-буфера "выбрасывается" на экран
Очистить сцену можно вызвав метод IDirect3DDevice8::Clear(). Его первые два параметра мы обнуляем, т.к. хотим очистить экран (на самом деле не экран, а рабочую область окна, но так проще писать :) ) целиком. Значение третьего параметра (D3DCLEAR_TARGET) говорит о том, что очистка должна происходить цветом, заданным четвертым параметром (в нашем случае D3DCOLOR_XRGB(30,60,120)). Последние два параметра в нашем случае игнорируются. Поэтому, пишем:

g_pd3dDevice->Clear(0,NULL,D3DCLEAR_TARGET,D3DCOLOR_XRGB(30,60,120),1.0f,0);

Подготовка завершена, переходим к рендерингу в бэк-буфер:

  g_pd3dDevice->BeginScene();
  g_pd3dDevice->EndScene();

Методы IDirect3DDevice8::BeginScene() и IDirect3DDevice8::EndScene() показывают, в каком месте начинается, а в каком заканчивается процесс рендеринга. Все методы рендеринга, вызванные вне зоны ограниченной данными операторами, игнорируются.

Последний шаг - передача видеоданных на экран через франт-буфер. Это осуществляется с помощью метода IDirect3DDevice8::Present().

g_pd3dDevice->Present(NULL,NULL,NULL,NULL);

Первые два параметра задают прямоугольные области источника (бэк-буфера) и места назначения (франт-буфера), поскольку копируется целиком вся область, то эти параметры выставлены в ноль. Третий параметр - указатель на принимающее окно, в клиентскую часть которого происходит выполнение метода Present(). Поскольку мы используем структуру D3DPRESENT_PARAMETERS, то третий параметр выставляем в NULL. Четвертый параметр - никогда не используется, его всегда нужно выставлять в NULL :-)

Функция Sweep()

Самая короткая по программному коду функция. Осуществляет "подчистку" после нашей работы.

  g_pd3dDevice->Release();
  g_pD3D->Release();      
  UnregisterClass("InitClass",wclass.hInstance);

Все просто. Сначала освобождаем устройства рендеринга, затем объект D3D. После этого остается лишь удалить класс окна и все!

Функция MessageProc()

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

switch (msg)
{
  case WM_DESTROY:
    Sweep();
    PostQuitMessage(0);
    break;
  case WM_PAINT:
    Render();
    ValidateRect(hWnd,NULL);
    break;
  default:
    return DefWindowProc(hWnd, msg, wParam, lParam);
    break;
}

Если окну поступает сообщение WM_DESTROY, то происходит "подчистка" функцией Sweep(), а обращение к функции PostQuitMessage(0) говорит системе, что наше приложение завершило работу. Если же окну поступило сообщение WM_PAINT (например, когда пользователь передвинул окно мышкой), то все окно должно быть немедленно перерисовано, что и делает наша функция Render(). Остальные сообщения обрабатываются стандартным образом. Как мне приятно описывать эту функцию :-) И не потому, что она последняя в данной статье. Просто в ней все красиво, т.к. ранее мы подготовили базу для ее работы.

Ну, вот и все. Надеюсь, что ты почерпал из этой статьи для себя что-то новое и интересное... Не пугайся, если программа показалась тебе слишком большой и сложной. Это не значит, что для рисования треугольника придется добавлять к ней такой же большой кусок кода. Просто дописать пару десятков строчек кода и делов-то :-) Ты сделал самый сложный шаг - первый. Теперь будет легче. Позже будем учиться отрисовывать трехмерные объекты, научимся использовать матрицы трансформации, устанавливать источники света, накладывать текстуры... Но об этом уже в следующих статьях...

Удачной тебе компиляции!

Примечание: Если будешь писать программу сам, помни, что для ее компиляции нужно подключить библиотечные файлы [*.lib]. Lib'ы подключайются так:

a. Project->Settings...
b. Открой вкладку "Link"
c. В строке "Object/library modules" добавь в начало d3d8.lib
d. Должно получиться примерно следующее: "d3d8.lib kernel32.lib user32.lib gdi32.lib..."

#Direct3D, #DirectX, #DirectX Graphics, #основы, #уроки

23 мая 2002