Данный материал взят с сайта old.shatalov.su и является его зеркалом

Создаём компьютерную игру. Создание игр на C++/DirectX

Есть вопросы?
Ошибка на сайте?
рус eng esp
Внимание! Данный сайт не обновляется. Новая версия: shatalov.su

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

Дата создания: 2009-12-31 12:08:10
Последний раз редактировалось: 2012-03-01 01:03:50

Последний раз мы выводили текст в консоли - в Морском бое. Кстати, если вы ещё не разобрались с морским боем, то настоятельно рекомендую это сделать, так как в следующем уроке будет упражнение, в котором необходимо портировать морской бой из консоли в Direct3D.

D3DX

D3DX - это дополнительный модуль к Direct3D (от Direct3D Extension; extension - расширенный). D3DX содержит вспомогательные интерфейсы, функции и структуры, которые облегчают работу с графикой в DirectX. Например, в D3DX содержится огромное количество математических функций, в том числе и матрица вращения вокруг произвольной оси. В состав D3DX входит также множество структур: D3DXMATRIX - матрица размером 4x4, D3DXVECTOR4 - четырёхмерный вектор. Если вы смотрели справку DirectX, то возможно заметили. что в разделе Direct3D Reference → Structures есть структуры и для вектора (D3DVECTOR) и для матрицы (D3DMATRIX - помните мы её использовали?). Но если сравнить D3DVECTOR И D3DXVECTOR4, то можно увидеть, что структура D3DXVECTOR4 включает в себя не только компоненты вектора, но и несколько перегруженных операторов.

Описание D3DX в документации расположено в разделе: DirectX Graphics → Direct3D 9 → Reference → D3DX Reference.

Если вы скачивали DirectX с сайта Microsoft, то справка находится в папке куда был установлен DirectX \Documentation\DirectX9\directx_sdk.chm. Или вы можете скачать справку к DirectX из раздела Листинги и программы на сайте.

Документацию по D3DX также можно найти и в MSDN, по адресу: http://msdn.microso ft.com/en-us/library/ee418036(VS.85).aspx.

Все интерфейсы D3DX начинаются с префикса ID3DX.

Интерфейс ID3DXFont и GDI.

В DirectX не существует встроенных средств для вывода текста на экран. Есть два варианта: создать свою систему вывода текста (возможны очень разные решения) или использовать GDI. Сегодня мы рассмотрим второй вариант.

GDI (от Graphical Device Interface - графический интерфейс устройства) - система вывода графики (в том числе и текста) в WinAPI. Всё что мы видим на экране монитора при запуске операционной системы, нарисовано с помощью GDI.

Как и многое другое в Windows, GDI не самая быстрая вещь на свете.

Самый простой способ вывести текст в DirectX - использование интерфейса ID3DXFont (font - шрифт). Этот интерфейс как раз и использует GDI для вывода текста.

Прежде всего необходимо подключить к проекту библиотеку d3dx9.lib и включить заголовочный файл d3dx9core.h. Кстати, важное отличие Direct3D от D3DX: в Direct3D экземпляры всего лишь одного интефрейса можно получить через функцию, а вот в D3DX большинство интерфейсов создаётся через функции.

Функции D3DXCreateFont и D3DXCreateFontIndirect

Для получения экземпляра интерфейса ID3DXFont можно воспользоваться двумя функциями: D3DXCreateFont и D3DXCreateFontIndirect. Сначала рассмотрим заголовок первой:

HRESULT D3DXCreateFont(
  LPDIRECT3DDEVICE9 pDevice, // указатель на устройство IDirect3DDevice9
  INT Height,                // высота шрифта
  UINT Width, // ширина шрифта; если передать 0, то установится автоматически
  UINT Weight,               // толшина шрифта: от нуля, до тысячи
  UINT MipLevels,            // уровень MIP
  BOOL Italic,               // наклонный шрифт
  DWORD CharSet,             // кодировка
  DWORD OutputPrecision,     // точность вывода
  DWORD Quality,             // качество (сглаженный шрифт, ClearType...)
  DWORD PitchAndFamily,      // шаг и семейство шрифта
  LPCTSTR pFacename,         // имя шрифта (Arial, Times New Roman...)
  LPD3DXFONT * ppFont        // указатель на ID3DXFont
);

Большинство аргументов этой функции интуитивно понятны, рассмотрим только некоторые:

MipLevels - уровень MIP:
MIP - сокращение от латинского multum in parvo, что переводится как многое в малом. MIP используется не только для шрифтов, но и при выводе текстур. При этом для текстуры (картинки) создаётся несколько картинок разного размера, но содержащих одно и то же. Делается это для увеличения быстродействия: чем дальше объект, на который натянута текстура, расположен от камеры, тем меньшего размера (менее качественная) выводится текстура. MIP и связанные вещи (сглаживание, фильтрация) мы ещё будем обсуждать более подробно.

CharSet - кодировка (char - символ, set - набор, множество):
При создании проекта в Visual C++ 2008 по умолчанию используется кодировка unicode и если вы не меняли настройки проекта, то аргумент CharSet не будет иметь значения - можно установить любой. Вот возможные значения этого аргумента: ANSI_CHARSET (английская кодировка ANSI), RUSSIAN_CHARSET (русская кодировка), DEFAULT_CHARSET (кодировка установленная в операционной системе по умолчанию) и другие.

OutputPrecision - точность вывода:
Данный аргумент определяет насколько точно будут соблюдены другие параметры функции при выводе: размер, тип, шаг шрифта...

PitchAndFamily - шаг и семейство шрифта:
Аргумент задаётся двумя константами. Шаг шрифта - ширина между буквами (забыл как правильно это называется). Возможные значения шага: FIXED_PITCH , VARIABLE_PITCH и другие. Возможные значения семейства: FF_ROMAN, FF_SCRIPT, FF_DONTCARE (не важно какой) и другие.

Теперь функция D3DXCreateFontIndirect:

HRESULT D3DXCreateFontIndirect(
  LPDIRECT3DDEVICE9 pDevice,
  CONST D3DXFONT_DESC * pDesc,
  LPD3DXFONT * ppFont
);

Данная функция создаёт шрифт косвенно (indirect - косвенно, не напрямую). Это значит, что все свойства шрифта задаются не через саму функцию (как в D3DXCreateFont), а через дополнительную структуру D3DXFONT_DESC (DESC от description - описание):

typedef struct D3DXFONT_DESC {
  INT Height;
  UINT Width;
  UINT Weight;
  UINT MipLevels;
  BOOL Italic;
  BYTE CharSet;
  BYTE OutputPrecision;
  BYTE Quality;
  BYTE PitchAndFamily;
  TCHAR FaceName[LF_FACESIZE];
} D3DXFONT_DESC, *LPD3DXFONT_DESC;

Здесь те же самые параметры, что и в функции D3DXCreateFont, только они собраны в отдельную структуру.

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

Более подробное описание параметров (и их возможных значений) шрифта можно найти в MSDN, на странице справки по структуре LOGFONT (от Logical font - логический шрифт; LOGFONT использовался раньше вместо D3DXFONT_DESC, но был заменён для увеличения скорости).

Рассмотри пример создания шрифта каждой из функций:

D3DXFONT_DESC fontDesc;
ZeroMemory(&fontDesc,sizeof(D3DXFONT_DESC));
fontDesc.Height = 18;
fontDesc.Width = 0;    // ширина задаётся автоматически, в зависмости от высоты
fontDesc.Weight = 400; // 400 - стандартная толщина шрифта
fontDesc.MipLevels = 0;
fontDesc.Italic = 0;
fontDesc.CharSet = RUSSIAN_CHARSET;
fontDesc.OutputPrecision = OUT_DEFAULT_PRECIS;
fontDesc.Quality = ANTIALIASED_QUALITY;   // сглаженный шрифт
fontDesc.PitchAndFamily = DEFAULT_PITCH | FF_DONTCARE;
wchar_t FaceName[] = L"Arial"; // можно подставить Times New Roman, Serif или любой др.
memcpy(&fontDesc.FaceName,&FaceName,sizeof(FaceName));

ID3DXFont* font;
D3DXCreateFontIndirect(videocard,&fontDesc,&font);
D3DXCreateFont(videocard,18,0,400,0,0,RUSSIAN_CHARSET,
               OUT_DEFAULT_PRECIS,ANTIALIASED_QUALITY,
               DEFAULT_PITCH | FF_DONTCARE,L"Arial",&font);

Обратите внимание на последние два оператора: нужно вызвать только одну из этих функций на выбор. Т.е. один из вызовов необходимо закомментировать. В данном примере обе функции создадут одинаковый шрифт.

Метод ID3DXFont::DrawText

Для вывода текста нужно воспользоваться методом ID3DXFont:

INT DrawText(
  LPD3DXSPRITE pSprite, // спрайт
  LPCTSTR pString,      // строка символов
  INT Count,            // количество символов
  LPRECT pRect,         // указатель на структуру RECT
  DWORD Format,         // формат (выравнивание текста)
  D3DCOLOR Color        // цвет
);

pSrpite:
Аргумент позволяет ускорить вывод текста. Пока что мы будем передавать NULL.

pRect:
Указатель на RECT. Этот параметр задаёт местоположение текста в окне программы.

Format:
Аргумент позволяет с помощью набора флагов задать форматирование текста. Мы будем выравнивать текст по левому/верхнему краю прямоугольника RECT: DT_TOP | DT_LEFT. Другие возможные значения данного параметра вы можете найти на странице справки по методу ID3DXFont::DrawText.

Ну а теперь вывод:

RECT rect;
rect.top = 10;
rect.left = 10;
rect.bottom = 35;
rect.right = 200;
wchar_t str[11] = L"fps:  0000";

font->DrawText(NULL,str,10,&rect,DT_TOP|DT_LEFT,0xff000000);

Здесь хочу обратить ваше внимание на следующий момент: для вывода строки используется тип wchar_t. Этот тип занимает два байта, а не один как char. Это позволяет хранить в wchar_t символы Unicode. Перед константой "fps: 0000" стоит буква L. Это значит, что текстовая константа приводится к типу wchar_t.

И последнее. Метод ID3DXFont::DrawText нужно рисовать уже после построения сцены - после методов BeginScene и EndScene, но до метода Present. Т.е. в фоновом буфере уже содержится картинка, а мы просто сверху рисуем текст. По сути, здесь происходит то же самое, что и в программе Спрайты. Часть первая - копирование картинки в фоновый буфер. Только там мы получали доступ к фоновому буферу напрямую, а здесь - через интерфейс ID3DXFont.

Заключение

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

    Упражнения
  1. В первом упражнении вам нужно создать счётчик кадров. Прежде всего вам понадобится прочитать урок про таймеры (доступен на главной странице).

    В программе необходимо выводить количество кадров в секунду (fps - frames per second).
    Думаю, количество fps вы легко подсчитаете. Самым сложным будет вывод значения на экран. Для понимания того, как правильно сохранить значение типа int (или любого другого числового значения) в строке текста, нужно вспомнить, что такое строки в C++. А строки в C++ - это массив чисел, каждое из которых представляет символ (символы закодированы с помощью одной из кодировок: ANSI, Unicode...). Рассмотрим пример:

    int fps; // количество кадров в секунду
    wchar_t str[] = L"fps:  0000"; // строка
    
    // начало основного цикла
    
    // увеличение fps
    if (dt > 1000)
    {
      // копирование значения fps в str
      fps = 0;
      t1 = /* получение текущего времени*/;
    }

    Каждый кадр происходит увеличение fps. Далее происходит проверка dt (как вычисляется dt рассказывается в уроке про таймеры). Если прошло больше секунды, то значение fps копируется в str, а fps обнуляется. То есть, значение на экране будет обновляться раз в секунду. Это конечно же не самый оптимальный вариант, но зато самый простой.

    Теперь осталось понять как заполнить последние несколько символов строки. В своём примере (я, кстати, воспользовался программой Камера и рекомендую писать счётчик кадров именно в ней) я использовал цикл do while (мы его рассмотрели, но ни разу ещё не использовали). Полный код я вам не дам. Только небольшую подсказку. Отдельные цифры можно вытащить из числа следующим образом:
    1. Берётся остаток деления числа на десять и сохраняется в отдельную переменную - это младший разряд.
    2. Далее из числа отнимается младший разряд и результат делится на десять.
    3. Первые два пункта повторяются, пока число не станет равным нулю.
    Если будут проблемы/вопросы при выполнении упражнения, не стесняйтесь - спрашивайте.

  2. После того, как вы напишете счётчик кадров и запустите программу, вы увидите, что счётчик показывает небольшое число. У меня было число шестьдесят. Это число - частота мерцания монитора.

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

    Давайте разберёмся, почему так происходит. Когда вызывается метод ID3DDevice9::Present, содержимое фонового и основного буфера меняются местами. Но чтобы картинка фонового буфера попала в основной, необходимо чтобы основной буфер был свободен. Чем же так занят основной буфер, что в него нельзя скопировать данные? Ответ очевиден: его содержимое выводится на экран. Сколько времени требуется, чтобы полностью вывести содержимое основного буфера на экран? 1/60 секунды при частоте монитора в 60 герц.

    Здесь мы видим пример так называемого горлышка бутылки: программа не может выполняться быстрее чем её самая медленная часть. Но процессоры и компьютера, и видеокарты могут работать в сотни (тысячи) раз быстрее чем 60 герц.

    Для того чтобы ускорить программу и позволить CPU (процессор компьютера, Computer Processor Unit - компьютерный вычислительный модуль) и GPU (процессор видеокарты, Graphical Processor Unit - графический вычислительный модуль) работать с максимальной скоростью, нужно ограничить количество вызовов ID3DDevice9::Present.

    Теперь упражнение: в зависимости от частоты вашего монитора, ограничьте количество вызовов метода Present. Т.е. метод Present должен вызываться не каждую итерацию цикла, а раз в 1/60 секунды (для 60 герц). Код, который вам понадобится написать, практически полностью был приведён в начале урока о таймерах.

    Вот что получилось у меня:

    Число в левом верхнем углу - скорость выполнения программы. Т.е. за одну секунду на моём компьютере основной цикл выполняется 6878 раз. Все следующие программы будут содержать счётчик итераций цикла. Это позволит нам отслеживать скорость выполнения программы и видеть, с какой скоростью работает программа и в какие моменты происходит падение скорости.

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

    На сегодня всё. До скорой встречи.