• DirectX 8: Начинаем работу с DirectX Graphics
  • DirectX 8: Создание и текстурирование простого трехмерного объекта
  • Первые шаги под DirectX 8. (Часть 1)
  • Первые шаги под DirectX 8. (Часть 2)
  • GameDev.ru

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

    (Автор: voxatu)

    (пример написан на основе первого "родного" туториала к 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…"

    (Автор: voxatu. )

    DirectX 8: Создание и текстурирование простого трехмерного объекта

    (Автор: voxatu)

    Ты уже умеешь инициализировать Direct3D, а значит можно на этой базе создавать трехмерные объекты. Чтобы было совсем интересно, натянем на наш трехмерный объект текстуру и заставим его вращаться!!! Кроме того, воспользуемся фильтрацией текстур и MipMapping'ом, чтобы получить более реалистичный результат почти без потери быстродействия. Попутно, научимся работать со вспомогательной библиотекой Direct3DX. Пример можешь скачать ЗДЕСЬ.

    Итак, приступим. За "трехмерный объект" возьмем правильную четырехугольную пирамиду, а за текстуру — подредактированную фотографию каменной стены (первоначальный вариант текстуры взят с сайта http://www.3dcafe.com/, на котором собрано огромное количество различных бесплатных для скачивания изображений).

    Функции WinMain(), MessageProc() и InitD3D() будут очень похожи на аналогичные функции программы из предыдущей статьи, поэтому я буду объяснять, в основном, только модифицированные их части. Структура программы немного изменилась. Теперь более грамотно обрабатываются ошибки, которые могут возникнуть при выполнении той или иной части программы.

    Рассмотренные функции исходника:

    Функция WinMain()

    Функция Initialization()

    Функция InitD3D()

    Функция InitTexture()

    Функция InitScene

    Функция DoMatrices()

    Функция RenderScene()

    Функция Deinitialization()

    Функция MessageProc()

    Функция WinMain()

    Главная функция нашего приложения. Практически аналогична одноименной функции из предыдущей статьи. Единственное, что сильно изменилось — цикл обработки сообщений. Также, вынесены в отдельные функции инициализация и деинициализация приложения.

    if (Initialization(hWnd)) {

     ShowWindow(hWnd, nCmdShow);

     UpdateWindow(hWnd);

     MSG msg;

     ZeroMemory(&msg, sizeof(msg));

     while(msg.message != WM_QUIT) {

      if (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)) {

       TranslateMessage(&msg);

       DispatchMessage(&msg);

      } else RenderScene();

     }

    }

    Deinitialization();

    Если инициализация проходит успешно, окно приложения отображается на экране и начинается цикл обработки сообщений, иначе деинициализация и выход из программы. Функция PeekMessage() проверяет очередь сообщений для текущей нити процесса и, если таковые имеются, помещает одно из них в структуру MSG. Флаг PM_REMOVE говорит о том, что сообщение из очереди удаляется, т.е. на нас возлагается ответственность его корректной обработки в любом случае. Это мы и делаем в следующих двух строчках. Если же очередь сообщений пуста, вызывается функция RenderScene(), которая, согласно ее названию, рендерит очередной кадр сцены. Как только поступает сообщение WM_QUIT, происходит деинициализация приложения (функция Deinitialization()).

    Функция Initialization()

    Осуществляет грамотную (т.е. с учетом ошибок) инициализацию D3D, текстур и сцены. Замечу, что все функции нашего приложения, в которых может возникнуть ошибка, имеют тип BOOL. Рассмотрим программный код:

    CurrentFilter = D3DTEXF_NONE;

    if (!InitD3D(hWnd)) return FALSE;

    if (!InitTexture()) {

     MessageBox(NULL, "Не найден файл текстуры!", "Ошибка", MB_ICONERROR);

     return FALSE;

    }

    if (!InitScene()) return FALSE;

    return TRUE;

    Что такое CurrentFilter? Забегая вперед, скажу, что наша программа будет использовать фильтрацию текстур, причем можно будет переключаться между различными фильтрами. Неплохо для первой программы с использованием D3D? ;-) Итак, первая строка кода говорит, что по умолчанию фильтром является D3DTEXF_NONE, т.е. фильтрация не используется. Затем вызываются три функции дополнительной инициализации, причем, если хотя бы в одной из них возникнет ошибка, функция Initialization() вернет значение FALSE, что в свою очередь остановит выполнение программы (вспомни функцию WinMain()) и приведет к деинициализации. Если функция InitTexture() возвращает значение FALSE, значит не удалось найти файл текстуры и перед аварийным завершением программы необходимо вывести предупредительное сообщение: MessageBox(NULL, "Не найден файл текстуры!", "Ошибка", MB_ICONERROR);

    Функция InitD3D()

    Согласно названию, эта функция инициализирует D3D %) Перед созданием устройства рендеринга, необходимо настроить параметры D3DPRESENT_PARAMETERS. К параметрам, которые мы использовали в предыдущем приложении добавилось лишь две строки:

    p_p.EnableAutoDepthStencil = TRUE;

    p_p.AutoDepthStencilFormat = D3DFMT_D16;

    Первая говорит о том, что при рендеринге будет использоваться Depth Buffer ("буфер глубины"), причем D3D будет управлять им автоматически. Когда разберешься с программой, попробуй поэкспериментировать: запусти программу в исходном виде, а затем проверь, что получится, если эти две строки удалить. Depth Buffer еще называют Z-Buffer'ом.

    Рассмотрим принцип его использования на примере. Представь себе человека на фоне кирпичной стены… Теперь мысленно переведи эту "сцену" в Direct3D (для этого, необходимо задать человека и стену в виде набора текстурированных полигонов) и заставь его эту сцену отрендерить. То, что мы увидим на экране — всего лишь двумерная проекция нашей сцены. Т.о., D3D спроектировал на плоскость экрана стену и человека. Причем, мы увидели человека на фоне стены, а не стену на фоне человека, т.к. он стоит ближе к нам Ж-) Как же D3D "узнает" что именно проектировать на экран раньше, чтобы не было бессмысленных перекрытий? Для этого используется Z-Buffer! Z-Buffer представляет собой массив Z-координат каждой точки этой поверхности (будем называть ее числовой составляющей (ЧС) Z-Buffer'а). Ось Z направлена в плоскость экрана (от тебя). Каждой точке сопоставлен единственный элемент ЧС. Перед началом рендеринга Z-буфер может быть заполнен в каждой точке значением равным максимально возможным расстоянием от наблюдателя до объекта сцены. После этого, для каждого полигона в сцене выполняются следующие действия:

    a. определяется область, которую будет занимать полигон при проектировании на плоскость экрана

    b. для каждого пикселя этой области сравнивается Z-координата точки-прообраза, принадлежащей исходному полигону, с соответствующим значением ЧС

    c. если соответствующее значение ЧС оказывается меньше, значит, рассматриваемая точка полигона не отображается, т.к. перекрыта другими объектами. Иначе, в ЧС для рассматриваемой точки записывается ее Z-координата, а в цветовую область заносится пиксель соответствующего цета.

    Вот и весь принцип.

    Далее в программе создается устройство рендеринга:

    if (FAILED(g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,  D3DCREATE_SOFTWARE_VERTEXPROCESSING, &p_p, &g_pD3DDevice))) {

     if (FAILED(g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_REF, hWnd,  D3DCREATE_SOFTWARE_VERTEXPROCESSING, &p_p, &g_pD3DDevice))) return FALSE;

    }

    Здесь программа пытается выбрать наиболее оптимальный режим для работы. Сначала она пробует создать аппаратное устройство рендеринга, т.е. когда все обсчеты графики ведет исключительно видеокарта (D3DDEVTYPE_HAL), но если это не удается (причин может быть много, но чаще всего она одна — старая видеокарта) D3D работает в режиме программной эмуляции, что прискорбно отражается на скорости и качестве графики. Едем дальше: Задается режим "выбраковки" тыльных сторон полигонов (подробней мы рассмотрим это дальше — в функции InitScene()):

    g_pD3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);

    Включаем равномерное освещение всей сцены:

    g_pD3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE);

    Включаем поддержку Z-Buffer'а

    g_pD3DDevice->SetRenderState(D3DRS_ZENABLE, TRUE);

    Может возникнуть вопрос, для чего может понадобиться отключение Z-Buffer'а. Одно из применений — вывод на экран текста, который должен перекрывать все остальное.

    Хотелось бы также подробней рассказать про функцию SetRenderState() интерфейса IDirect3DDevice8, которую мы уже встречали ранее. Эта функция вообще незаменима, т.к. с ее помощью можно задать огромное количество различных настроек, влияющих на процесс рендеринга. Вот описание этой функции:

    HRESULT SetRenderState(D3DRENDERSTATETYPE State, DWORD Value);

    State — тип изменяемого параметра

    Value — новое его значение

    Функция InitTexture()

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

    Вот описание функции D3DXCreateTextureFromFile():

    HRESULT D3DXCreateTextureFromFile(LPDIRECT3DDEVICE8 pDevice, LPCSTR pSrcFile, LPDIRECT3DTEXTURE8* ppTexture);

    pDevice — указатель на устройство рендеринга

    pSrcFile — текстовая строка, содержащая путь к файлу-текстуре

    ppTexture — адрес переменной, которая будет содержать указатель на текстуру

    Функция InitScene()

    Здесь происходит инициализация единственного объекта в сцене — четырехугольной пирамиды.

    Как известно, в основании правильной четырехугольной пирамиды (SABCD) лежит квадрат (ABCD). Если сторона квадрата a, то высота пирамиды h=a/sqrt(2). Теперь, зная a, мы можем задать в трехмерном пространстве координаты всех пяти вершин пирамиды. В DirectX используется левосторонняя (left-handed) система координат (СК). Направление оси Z в левосторонней и правосторонней СК можно определить, пользуясь правилом соответственно левой и правой руки. Вот, как с помощью этого правила найти куда направлена ось Z в левосторонней СК: вытяни левую руку ладонью вверх и собери четыре пальца (все, кроме большого) вместе в плоскости ладони. Большой палец расположи перпендикулярно остальным четырем (тоже в плоскости ладони). Отлично! (Ты смог это! - прим. редактора ) Теперь, если направить ось X вдоль четырех пальцев, а ось Y - вверх, то большой палец укажет направление оси Z.

    Координаты вершин пирамиды в пространстве можно записать следующим образом (не обращай пока внимание на последние 3 параметра каждой вершины):

    float a=6.0;

    #define vertA {-a/2, a/2, 0.0f, 0xffffffff, 0.0f, 1.0f,}

    #define vertB {-a/2, -a/2, 0.0f, 0xffffffff, 0.0f, 0.0f,}

    #define vertC {a/2, -a/2, 0.0f, 0xffffffff, 1.0f, 0.0f,}

    #define vertD {a/2, a/2, 0.0f, 0xffffffff, 1.0f, 1.0f,}

    #define vertS {0.0f, 0.0f, (float)(a/sqrt(2)), 0xffffffff, 0.5f, 0.5f,}

    В D3D трехмерную модель можно задать различными способами. В нашем случае будем использовать TRIANGLELIST для которого фигура задается последовательностью треугольников. Когда задается треугольник, Direct3D сам определяет его лицевую и тыльную стороны по порядку следования вершин в массиве. При рендеринге, D3D автоматически "выбраковывает" тыльные стороны треугольников. Это заметно повышает скорость работы приложения. Но необходимо указать D3D в каком именно порядке задаются вершины лицевой стороны треугольников. Это делается с помощью той самой волшебной функции SetRenderState().

    Итак, чтобы описать culling (выбраковку) тыльных сторон треугольников, вершины которых расположены в массиве по часовой стрелке (clockwise), необходимо написать следующее:

    g_pD3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);

    Для хранения вершин в D3D используются Vertex Buffer'ы (в дальнейшем, VB. Не путать с Visual Basic'ом :)). В зависимости от конкретной программы, VB'ы могут быть разных форматов. Например, если требуется написать программу, которая рисует на экране набор одноцветных точек, то для задания любой из точек требуется три числа, содержащих ее координаты в пространстве. Если точки должны отличаться по цвету, вводим четвертый параметр — цвет точки. Вроде бы все просто… Единственная сложность — мы как-то должны "сообщить" D3D в каком именно формате хранятся вершины в массиве, чтобы в процессе рендеринга не возникло путаницы. Впервые это нужно сделать в момент создания VB, затем перед рендерингом. Формат задается в виде комбинации флагов D3DFVF_*, полный список которых приведен в документации к D3D8. Нам же понадобятся лишь 3 флага:

    #define D3DFVF_MYVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1)

    D3DFVF_XYZ — вершина задается тремя координатами в пространстве (а может задаваться и четырьмя — при D3DFVF_XYZRHW)

    D3DFVF_DIFFUSE — вершина содержит цвет, который влияет на рассеяние света

    D3DFVF_TEX1 -—вершина содержит две текстурные координаты

    Т.к. запись

    D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1

    интерпретируется компилятором в точности, как и

    D3DFVF_XYZ | D3DFVF_TEX1 | D3DFVF_DIFFUSE

    значит порядок расположения данных в памяти в этом месте программы не задается. На программиста накладываются обязательства следовать схеме расположения данных, приведенной в руководстве D3D8 (раздел "About Vertex Formats").

    Ну, надеюсь с этим все ясно. Теперь нужно занести вершины всех полигонов пирамиды в память. Для этого создаем массив из вершин и заполняем его данными:

    MYVERTEX Vertices[] = {

     vertS, vertA, vertD,

     vertS, vertB, vertA,

     vertS, vertC, vertB,

     vertS, vertD, vertC,

    };

    Следующий шаг — нужно создать буфер вершин (VB) требуемого размера и формата. Пирамида будет отображена на экране так, что ее нижнего основания не будет видно, значит можно обойтись лишь 4-мя полигонами вместо 6-ти. Здесь я следовал правилу, которое прочитал в руководстве DX: "Remember, the fastest polygons are the ones you don't draw" (что в переводе означает: "Помни, наиболее быстрые полигоны — это те, которые ты не рисуешь"). Создание VB производится функцией CreateVertexBuffer:

    HRESULT CreateVertexBuffer(UINT Length, DWORD Usage, DWORD FVF, D3DPOOL Pool, IDirect3DVertexBuffer8** ppVertexBuffer);

    Length — длина VB в байтах

    Usage — дополнительная информация о VB, которую D3D использует для создания оптимального VB

    FVF — формат вершин, которые будут храниться в VB

    Pool — в какой памяти создавать VB (можно создать его как в видеопамяти, так и в RAM)

    ppVertexBuffer — адрес переменной, которая будет содержать указатель на созданный VB

    Всего для хранения полигонов пирамиды используется 4*3*sizeof(MYVERTEX) байт (4 полигона, по 3 вершины в каждом).

    if (FAILED(g_pD3DDevice->CreateVertexBuffer(4*3*sizeof(MYVERTEX), 0, D3DFVF_MYVERTEX, D3DPOOL_DEFAULT, &g_pVB))) {

     return FALSE;

    }

    Остается заполнить буфер вершинами. Для операций заполнения в DX (не только в D3D) используется пара команд Lock() и Unlock(). Команда Lock() возвращает адрес памяти, по которому расположен первый байт буфера. При этом вся память, отведенная под буфер как бы "запирается", и становится недоступной для других приложений. Операция "отпирания" памяти производится командой Unlock(). После запирания памяти, скопируем данные с помощью Си'шной функции memcpy().

    VOID* pVertices;

    if (FAILED(g_pVB->Lock(0, sizeof(Vertices), (BYTE**)&pVertices, 0))) return FALSE;

    memcpy(pVertices, Vertices, sizeof(Vertices));

    g_pVB->Unlock();

    Функция DoMatrices()

    Я считаю, что это — самая сложная для понимания функция. Разговор о матрицах выходит за формат данной статьи, т.к. это очень обширная тема. Советую почитать статьи JM'а по этому поводу (скажу по секрету — он фанат матриц ;o)). Но вкратце, я все равно расскажу о матрицах :-)

    У нас есть трехмерное пространство сцены, которое содержит вершины всех объектов, есть камера - глаз, с помощью которого мы видим это пространство, а также плоскость экрана монитора, на которую осуществляется проектирование. Все это ("мир", камера, операция проектирования) может быть выражено тремя матрицами: World Matrix (мировая матрица), View Matrix (видовая матрица) и Projection Matrix (проекционная матрица).

    Вычислять эти матрицы "вручную" довольно сложно, поэтому воспользуемся функциями D3DX. Для матриц создан специальный тип данных D3DMATRIX. В библиотеке D3DX он расширен до типа данных D3DXMATRIX, в который добавлены арифметические операции с матрицами, и некоторые другие удобные свойства.

    • Функция D3DXMatrixIdentity() строит единичную матрицу.

    • Функция D3DXMatrixRotationZ() строит матрицу вращения относительно оси Z на заданный угол.

    • Функция D3DXMatrixLookAtLH() строит видовую матрицу. Параметры этой функции задают точку, в которую будет смотреть камера. Постфикс -(LH) говорит о том, что матрица будет действительна для левосторонней системы координат (аналогично -(RH) для правосторонней)

    • Функция D3DXMatrixPerspectiveFovLH() строит проекционную матрицу.

    Для того, чтобы "заставить" устройство рендеринга использовать только что созданные нами матрицы, существует функция SetTransform():

    HRESULT SetTransform(D3DTRANSFORMSTATETYPE State, CONST D3DMATRIX* pMatrix);

    State — тип матрицы, которую нужно изменить (мировая, видовая, проекционная и т.д.)

    pMatrix — указатель на "матрицу-заменитель" :)

    Вот что нам требуется от каждой из матриц:

    a. Мировая

    Сделаем так, чтобы пирамида с течением времени равномерно вращалась вокруг оси Z:

    D3DXMATRIX matWorld;

    D3DXMatrixIdentity(&matWorld);

    D3DXMatrixRotationZ(&matWorld, GetTickCount()/1024.0f);

    g_pD3DDevice->SetTransform(D3DTS_WORLD, &matWorld);

    b. Видовая

    Камера должна смотреть на пирамиду сбоку, причем не должно быть видно нижнего основания пирамиды (помнишь, мы выбросили два полигона основания?):

    D3DXMATRIX matView;

    D3DXMatrixLookAtLH(&matView, &D3DXVECTOR3(5.0f, 5.0f, 6.5f), &D3DXVECTOR3(0.0f, 0.0f, 1.0f), &D3DXVECTOR3(0.0f, 0.0f, 1.0f));

    g_pD3DDevice->SetTransform(D3DTS_VIEW, &matView);

    c. Проекционная

    D3DXMATRIX matProj;

    D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI/3, 1.0f, 1.0f, 100.0f);

    g_pD3DDevice->SetTransform(D3DTS_PROJECTION, &matProj);

    Здесь D3DX_PI/3 - это поле зрения (field of view) камеры. Попробуй поэкспериментировать с этим параметром.

    Функция RenderScene()

    Собственно, здесь и происходит рендеринг сцены. Как всегда, он начинается с очистки окна и Z-Buffer'а:

    g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(100, 100, 100), 1.0f, 0);

    Затем, подготавливаем D3D к началу сцены.

    g_pD3DDevice->BeginScene();

    Пересчитываем матрицы:

    DoMatrices();

    Теперь настроим текстуру. И вообще, здесь подходящее место для того, чтобы вкратце рассказать о том, что такое текстура и текстурные координаты! Итак… Текстура — это графическая картинка, которая используется для натягивания на трехмерный (и не только) объект, что придает ему реалистичный вид (правда, это зависит от текстуры и от того, кто и на что ее натягивает :)) ). При текстурировании объекта, каждая его вершина должна иметь текстурные координаты, т.е. числа от 0 до 1, задающие привязку к конкретному месту текстуры. Рассмотрим пример, т.е. нашу четырехугольную пирамиду. Она была выбрана не случайно именно четырехугольной! Представь себе квадратный (для простоты), очень эластичный лист резины, на котором изображена каменная стена (текстура). Теперь "схвати" его за центр и тяни вверх (при этом края квадрата должны оставаться на месте). Т.к. лист эластичный, он легко поддастся и начнет растягиваться. Вместе с ним будет растягиваться и изображение стены. Тяни до тех пор, пока лист не превратится в правильную четырехугольную пирамиду без нижнего основания. Заморозь полученный объект, чтобы он не вернулся в первоначальное положение. Все! Еще раз вернемся к месту, где задаются вершины пирамиды:

    float a=6.0;

    #define vertA {-a/2,  a/2, 0.0f, 0xffffffff,  0.0f, 1.0f,}

    #define vertB {-a/2, -a/2, 0.0f, 0xffffffff, 0.0f, 0.0f,}

    #define vertC {a/2, -a/2, 0.0f, 0xffffffff,  1.0f, 0.0f,}

    #define vertD {a/2, a/2, 0.0f, 0xffffffff,  1.0f, 1.0f,}

    #define vertS {0.0f, 0.0f, (float)(a/sqrt(2)), 0xffffffff, 0.5f, 0.5f,}

    0xffffffff (белый цвет) — цветовой "вес" вершины. Белый цвет означает, что текстура в данной вершине будет того же цвета и яркости, что и в оригинале. Если заменить цвет на 0x00000000, то вершина будет черной и при рендеринге вокруг нее образуется черное пятно. Попробуй! =)

    Последние два числа являются текстурными координатами. Вернемся к мысленному эксперименту с квадратным листом… (до того, как мы его деформировали) Возьмем его левый верхний угол за начало координат. Ось X направим вправо, ось Y — вниз. Тогда правый нижний угол будет иметь координаты (1, 1). Как ты уже догадался, центр квадрата (проекция вершины S пирамиды на плоскость основания) имеет координаты (0.5, 0.5). Вот так задаются текстурные координаты.

    Вернемся к программе. У нас будет одноуровневая текстура, первым и единственным уровнем которой будет загруженная ранее g_pTexture:

    g_pD3DDevice->SetTexture(0, g_pTexture);

    При рендеринге на текстуры могут накладываться фильтры, что сглаживает многие недостатки. Изюминкой программы является то, что тип фильтра можно менять прямо во время исполнения (клавишами F1, F2, F3, F4). Кроме того, как я обещал в начале статьи, используем MipMapping! Но для начала расскажу, что он собой представляет…

    MipMap - это цепочка текстур, каждая последующая из которых является менее детализированным вариантом предыдущей. Уменьшение детализации на один уровень достигается путем сокращения длины и ширины текстуры в два раза. Цепочка генерируется до тех пор, пока размер одной из сторон текстуры не становится равным 1. Допустим, что текстурированный объект удаляется от наблюдателя. Сначала на него накладывается текстура с максимальным разрешением, затем, по мере удаления, текстура переключается на свой менее детализированный вариант. Согласись, на далекий объект, который занимает на экране всего один пиксель, глупо натягивать текстуру размером 100 Kb. Таким образом, благодаря некоторым дополнительным затратам памяти на MipMap-текстуры, заметно увеличивается быстродействие рендеринга и, вообще говоря, его качество. Во время переключения между детализациями, визуально может быть заметен скачок. Его можно сгладить, используя фильтрацию, что мы и сделаем (напомню, что текущий тип фильтра хранится в переменной CurrentFilter):

    g_pD3DDevice->SetTextureStageState(0, D3DTSS_MAGFILTER, CurrentFilter);

    g_pD3DDevice->SetTextureStageState(0, D3DTSS_MINFILTER, CurrentFilter);

    g_pD3DDevice->SetTextureStageState(0, D3DTSS_MIPFILTER, CurrentFilter);

    Перед рендерингом из VB, необходимо задать сам буфер и формат вершин, что делается следующими двумя строками:

    g_pD3DDevice->SetStreamSource(0, g_pVB, sizeof(MYVERTEX));

    g_pD3DDevice->SetVertexShader(D3DFVF_MYVERTEX);

    И, наконец! Все готово для рендеринга! Делается это всего одной функцией DrawPrimitive(). Первый параметр говорит о том, в каком виде хранятся в VB вершины. В нашем случае, объект задается последовательностью треугольников (D3DPT_TRIANGLELIST). Второй параметр говорит о том, с какой по номеру вершины из VB начинать отрисовку. Третий параметр — количество примитивов (в нашем случае треугольников), которые требуется отрендерить.

    g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 4);

    Завершаем сцену:

    g_pD3DDevice->EndScene();

    Отображаем бэк-буфер на экране:

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

    Функция Deinitialization()

    Для освобождения большинства (если не всех) видов ресурсов в D3D используется функция Release():

    if (g_pTexture != NULL) g_pTexture->Release();

    if (g_pVB != NULL) g_pVB->Release();

    if (g_pD3DDevice != NULL) g_pD3DDevice->Release();

    if (g_pD3D != NULL) g_pD3D->Release();

    Также, перед завершением работы нужно освободить ранее зарегистрированный класс окна:

    UnregisterClass("PyramidClass", wclass.hInstance);

    Функция MessageProc()

    Программа будет обрабатывать только два типа сообщений: WM_KEYDOWN (нажата клавиша) и WM_DESTROY (уничтожено окно приложения).

    При поступлении сообщения WM_KEYDOWN получаем виртуальный код нажатой клавиши:

    case WM_KEYDOWN:

     int VK;

     VK = (int)wParam;

    Из клавиш, обрабатываем только Esc, F1, F2, F3, F4. При нажатии на Esc программа должна завершиться, как ни парадоксально это звучит :)) Если нажата клавиша F1-F4, должен смениться тип фильтра (переменная CurrentFilter) и заголовок окна:

    switch(VK) {

    case VK_ESCAPE:

     PostQuitMessage(0);

     return 0;

      //...

    case VK_F2:

     CurrentFilter = D3DTEXF_POINT;

     SetWindowText(hWnd, "D3D Pyramid (Filter=D3DTEXF_POINT)");

     break;

     //...

    }

    При поступлении сообщения WM_DESTROY, выполняются действия, аналогичные производящимся при обработке клавиши Esc:

    case WM_DESTROY:

     PostQuitMessage(0);

     return 0;

    Приехали! В принципе, вышеизложенного материала достаточно для самостоятельного написания простых 3D-приложений (для написания игры необходимо, как минимум, уметь работать с матрицами). Рекомендую поэкспериментировать с примером к данной статье. Попробуй поиграть настройками D3D, параметрами функций. Дай волю своему воображению! И, конечно же, попытайся написать что-нибудь сам (например, вращающийся треугольник или куб). Только так можно научиться. Накапливай опыт…

    Удачи!

    Примечания:

    1. Помни, что для компиляции программ, использующих D3DX, необходимо подключить библиотечный файл d3dx8.lib (вместе с d3d8.lib)! Еще раз напомню, как это можно сделать:

     a. Project->Settings…

     b. Вкладка "Link"

     c. В строке "Object/library modules" добавь в начало "d3d8.lib d3dx8.lib"

     d. Должно получиться примерно следующее: "d3d8.lib d3dx8.lib kernel32.lib user32.lib gdi32.lib…"

    2. Программе необходим файл texture.jpg — он должен лежать в той же директории, что и exe'шник.

    Полезные статьи:

    (Автор: voxatu.)

    Первые шаги под DirectX 8. (Часть 1)

    (Автор: Сергей Ваткин.)

    Первым делом вам нужно убедиться, что на вашей машине установлен компилятор C++ и DirectX8 SDK. Если у вас нет DirectX8 SDK, его можно скачать с сайта Microsoft, учтите только, что скачиваемый файл занимает примерно 140 Мб.

    Дальше необходимо немного подстроить вашу среду разработки, а именно указать рабочие пути к DirectX, то есть к заголовочным файлам (.h — файлам) и файлам статических библиотек (.lib). Например, В Visual C++ это делается следующим образом. Меню Tools/Options вызывает диалоговое окно Options. В нем выбираем закладку Directories и выбираем из списка Show directories for сперва Include files для пути к заголовочным файлам, затем добавляем путь (папка Include в каталоге, куда установлен DirectX SDK) и выставляем его, нажимая на иконку "стрелочка вверх", в первую позицию. Аналогично и для пути к библиотечным файлам, выбираем из списка Show directories for пункт Library files и добавляем путь к библиотечным файлам (директория LIB в каталоге DirectX SDK).

    Теперь попробуем написать простейшую программу, инициализирующую DirectX. Для этого познакомимся с некоторыми интерфейсами Direct3D и их методами.

    Для создания объекта d3d используется функция Direct3DCreate8():

    pD3D = Direct3DCreate8(D3D_SDK_VERSION);

    здесь pD3D — указатель на интерфейс IDirect3D8,

    D3D_SDK_VERSION
    объявлено в d3d8.h.

    Файл d3d8.h содержит объявления функций, работающих с Direct3D. Поэтому его необходимо включить в свой код директивой препроцессора:

    #include <d3d8.h>

    Функция Direct3DCreate8() возвращает указатель на созданный объект d3d. Если она вернула значение NULL, то это означает, что ваша программа не может инициализировать DirectX8. В этом случае ваша программа должна закончить своё выполнение.

    Компонент, который занимается просчётом объектов на экран в Direct3D, называется Direct3D device. Он также включает в себя и сохраняет установки, касающиеся просчёта, занимается операциями трансформации, освещения и растеризацией. Метод CreateDevice() интерфейса объекта d3d создаёт такой device. Одним из параметров этого метода является объект структурного типа D3DPRESENT_PARAMETERS. Для ознакомления с Direct3D, мы не будем углубляться в значения всех полей этой структуры. Скажем только про те, которые нам сейчас понадобятся. Поле Windowed отвечает за то, будет ли графика отображаться на весь экран или в окне; значение может быть соответственно FALSE или TRUE.

    Чтобы не было видно, как перерисовывается графика, все отображаемые объекты выводят сперва в невидимой области видеопамяти, так называемой back buffer (задний буфер). После того, как кадр полностью прорисован, его копируют в видимую область памяти — front buffer или render target. Если приложение отображает графику на весь экран, то в копировании нет необходимости, достаточно просто говорить Direct3D какая часть видеопамяти является видимой, то есть просто переключать (flip) видимость с одного буфера на другой. Ясно, что если мы выводим графику в окно, то мы можем пользоваться только копированием back buffer'а в front buffer. Поле SwapEffect мы выставим в значение d3dswapeffect_discard чтобы наиболее эффективно представить back buffer для отображения.

    BackBufferFormat — ещё одно поле структуры D3DPRESENT_PARAMETERS, которое нам необходимо задать, чтобы сказать Direct3D, какого формата будет back buffer. Пусть наше приложение, для простоты, будет выводить графику в окно. Формат back buffer'а должен совпадать с форматом front buffer'а, который в свою очередь в нашем случае совпадает с текущим форматом desktop'а. Его же мы можем получить методом интерфейса объекта d3d GetAdapterDisplayMode(), указав в качестве первого параметра D3DADAPTER_DEFAULT.

    Итак, мы можем уже написать функцию инициализации Direct3D для последующего просчёта объектов:

    // функция Init создаёт объекты d3d и d3d device

    bool Init(HWND hWnd) {

     if (pD3d = Direct3DCreate8(D3D_SDK_VERSION)) {

      D3DDISPLAYMODE d3ddm; // отсюда нам нужно поле Format

      if (FAILED(pD3d->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm)))

       return false;

      D3DPRESENT_PARAMETERS d3dpp;

      ZeroMemory(&d3dpp, sizeof(d3dpp)); // выставляем в 0 все поля d3dpp

      d3dpp.Windowed = TRUE;

      d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;

      d3dpp.BackBufferFormat = d3ddm.Format;

      if (FAILED(pD3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &pDevice))) {

       return false;

      }

      return true;

     }

     returnfalse;

    }

    Здесь pD3d — объект d3d, объявленный как

    IDirect3D8 * pD3d;

    а pDevice — указатель на интерфейс device'а

    IDirect3DDevice8 * pDevice;

    В методе CreateDevice() вторым параметром мы задали D3DDEVTYPE_HAL. Это означает, что мы пытаемся создать устройство просчёта, поддерживаемое hardware.

    Таким образом, если наша функция Init() вернула значение true, означает, что инициализация прошла успешно. После этого мы можем выводить графику средствами DirectX. Об этом мы поговорим во второй части урока. Единственное, что осталось добавить к выше сказанному, раз уж мы научились создавать объекты, нужно не забывать их удалять. Объекты DirectX являются обычными COM объектами. После того, как мы закончили ими пользоваться, необходимо вызвать методы Release(), для каждого из этих объектов:

    // функция ReleaseAll освобождает объекты d3d device и d3d

    void ReleaseAll() {

     // Освобождаются в обратном порядке создания

     if (pDevice) pDevice->Release();

     if (pD3d) pD3d->Release();

    }

    Поведал: Ваткин.

    Первые шаги под DirectX 8. (Часть 2)

    (Автор: Сергей Ваткин.)

    Попробуем вывести простейший объект в нашем приложении. Любой трехмерный объект, который может вывести DirectX, является полигональной моделью. То есть любой объект состоит из некоторого количества треугольников. Треугольники задаются своими вершинами. Вершины, в свою очередь, могут иметь какие-то координаты, цвет, текстурные координаты и т.д. Вершины могут быть разных типов, например, если мы не используем текстурирование, зачем нам задавать текстурные координаты. Зададим самый простой формат вершины. У вершины такого формата будут координаты положения в трёхмерном пространстве и цвет:

    #define D3DFVF_MYVERT (D3DFVF_XYZ | D3DFVF_DIFFUSE)

    Структура данных для такой вершины будет выглядеть следующим образом:

    struct MyVert{

     float x, y, z; // координаты

     DWORD Color; // диффузный цвет

    };

    Цвет вершины задаётся 32-х битовым целым числом. Для установки нужного цвета можно использовать макроподстановку D3DCOLOR_XRGB(r,g,b), где r,g и b — составляющие компоненты цвета, соответственно красная, зелёная и синяя, могут принимать целые значения от 0 до 255-ти.

    При помощи device вы управляете просчётом, то есть выводом графики. Чтобы заполнить каким-либо цветом, например, синим, всю область просчёта, можно воспользоваться методом Clear():

    DWORD dwBlue = D3DCOLOR_XRGB(0, 0, 128);

    pDevice->Clear(0, NULL, D3DCLEAR_TARGET, dwBlue, 1.0f, 0);

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

    pDevice->BeginScene();

    После того, как вы закончили посылать объекты на просчет, вызывайте ещё один специальный метод:

    pDevice->EndScene();

    То есть прорисовка всех объектов происходит между вызовами этих методов. Об этом нужно помнить, если вы попытаетесь вывести объект вне этого блока, то DirectX просто проигнорирует вас.

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

    pDevice->SetVertexShader(D3DFVF_MYVERT);

    Это необходимо для того, чтобы DirectX знал, как расположена информация о вершине в структуре MyVert и каким способом он будет выводить объекты.

    Треугольник задаётся последовательностью вершин, как линейный (одномерный) массив. Для одного треугольника нам понадобится три вершины:

    MyVert v[3];

    //Зададим расположение для вершин:

    v[0].x =-0.5f; v[0].y =-0.5f; v[0].z = 0.5f;

    v[1].x =-0.5f; v[1].y = 0.5f; v[1].z = 0.5f;

    v[2].x = 0.5f; v[2].y = 0.5f; v[2].z = 0.5f;

    //и цвет:

    v[0].Color = D3DCOLOR_XRGB(255,0,0); // красный

    v[1].Color = D3DCOLOR_XRGB(0,255,0); // зеленый

    v[2].Color = D3DCOLOR_XRGB(0,0,255); // синий

    Выводом объектов занимаются семейство методов DrawPrimitive интрефейса IDirect3DDevice8. Примитивом здесь называется минимальный объект, который может быть выведен таким методом, например, треугольник здесь тоже является примитивом. В нашем случае мы воспользуемся методом DrawPrimitiveUP():

    pDevice->DrawPrimitiveUP( D3DPT_TRIANGLELIST, 1, v, sizeof(MyVert));

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

    Поскольку всё отображение ведется в невидимую часть экрана, мы должны показать все это на экране. Этим занимается метод Present(). Для нашего примера параметры этой функции нас не интересуют, выставляем их в 0:

    pDevice->Present(0, 0, 0, 0);

    Этот метод вызывается, когда вы отобразили все объекты, участвующие в текущем кадре, в текущей сцене, то есть после метода EndScene().

    В итоге мы построили функцию Render(), которая и будет выводить наш треугольник на экран:

    void Render() {

     // Инициализацию массива v можно вынести за пределы

     // функции Render()

     MyVert v[3];


     v[0].x =-0.5f;  v[0].y =-0.5f;  v[0].z = 0.5f;

     v[1].x =-0.5f;  v[1].y = 0.5f;  v[1].z = 0.5f;

     v[2].x = 0.5f;  v[2].y = 0.5f;  v[2].z = 0.5f;


     v[0].Color = D3DCOLOR_XRGB(255,0,0); // красный

     v[1].Color = D3DCOLOR_XRGB(0,255,0); // зеленый

     v[2].Color = D3DCOLOR_XRGB(0,0,255); // синий


     // Закрашиваем экран синим цветом

     DWORD dwBlue = D3DCOLOR_XRGB(0,0,128);

     pDevice->Clear(0, NULL, D3DCLEAR_TARGET, dwBlue, 1.0f, 0);

     pDevice->BeginScene();

      // Поскольку мы не используем освещение для треугольника,

      // отключаем его

      pDevice->SetRenderState(D3DRS_LIGHTING, FALSE);

      // Просчет объектов всегда между BeginScene и EndScene

      pDevice->SetVertexShader(D3DFVF_MYVERT);

      pDevice->DrawPrimitiveUP(D3DPT_TRIANGLELIST, 1, v, sizeof(MyVert));

     pDevice->EndScene();

     pDevice->Present(0, 0, 0, 0);

    }

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

    Ниже приведён код, который больше относится к windows-программированию. Создаётся главное окно приложения и ведётся обработка сообщений:

    // функция обрабатывающая сообщения главного окна приложения

    LRESULT WINAPI MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {

     switch (msg) {

     case WM_DESTROY:

      PostQuitMessage(0);

      return 0;

     }

     return DefWindowProc(hWnd, msg, wParam, lParam);

    }


    INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR, INT) {

     WNDCLASSEX wc = {

      sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L,

      GetModuleHandle(0), 0, 0, 0, 0, "FirstDX_cl", 0

     };

     RegisterClassEx(&wc);

     // Создание главного окна приложения

     HWND hWnd = CreateWindow("FirstDX_cl", "FirstDX",

      WS_OVERLAPPEDWINDOW, 100, 100, 160, 160,

      GetDesktopWindow(), NULL, wc.hInstance, NULL);

     if (Init(hWnd)) {

      ShowWindow (hWnd, SW_SHOWDEFAULT);

      UpdateWindow(hWnd);

      MSG msg;

      ZeroMemory(&msg, sizeof(msg));

      while (msg.message != WM_QUIT) {

       if (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)) {

        TranslateMessage(&msg);

        DispatchMessage(&msg);

       } else Render();

      }

     }

     ReleaseAll();

     UnregisterClass("FirstDX_cl", wc.hInstance);

     return 0;

    }

    Функция Render() вызывается всегда, когда не приходят какие-либо сообщения, то есть перерисовка кадра происходит практически постоянно. Функции Init() и ReleaseAll() описаны в предыдущей части урока.

    Теперь есть всё, чтобы вы смогли скомпилировать и запустить наш пример. Не забудьте добавить библиотеку d3d8.lib в ваш проект, чтобы линковщик смог найти реализации функций Direct3D.

    Поведал: Ваткин.








    Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх