Проект содержит два уникальных файла: main.cpp и main.h. Остальные включенные в проект файлы, — d3dapp.cpp, d3denumeration.cpp, d3dfont.cpp, d3dsettings.cpp, d3dutil.cpp и dxutil.cpp, — являются частью Microsoft DirectX 9.0 SDK.
Уникальные файлы я создал специально для этого примера. Вы можете обратить внимание на префикс D3DFrame у названия проекта. Он означает, что при создании программы я пользовался предоставляемым Microsoft каркасом приложения Direct3D. Каждый раз, когда вы увидите этот префикс, знайте, что я использовал каркас приложения. Если вам не нравятся подобные каркасы, не волнуйтесь, — я покажу как выполнить ту же самую работу не применяя их.
Как видно на рисунке, проект включает собственные уникальные файлы, файлы каркаса приложения DirectX и библиотеки d3d9.lib, dxguid.lib, d3dx9dt.lib, d3dxof.lib, comctl32.lib и winmm.lib.
Заголовочный файл Main.h
Есть только один заголовочный файл, который следует обсудить подробно - файл main.h. Он содержит всю заголовочную информацию, необходимую для программы. Вот как выглядит содержащийся в этом файле код:
#define STRICT
#include
#include
#include
#include
#include
#include
#include
#include "DXUtil.h"
#include "D3DEnumeration.h"
#include "D3DSettings.h"
#include "D3DApp.h"
#include "D3DFont.h"
#include "D3DUtil.h"
// Структура для данных вершин блока
struct TILEVERTEX
{
D3DXVECTOR3 position; // Позиция
D3DXVECTOR3 vecNorm; // Нормаль
FLOAT tu, tv; // Координаты текстуры (U,V)
};
// Наш собственный FVF, описывающий созданную структуру данных вершин
// D3DFVF_XYZ= Информация о координатах
// D3DFVF_NORMAL = Информация о нормалях
// D3DFVF_TEX1 = Информация о текстуре
#define D3DFVF_TILEVERTEX (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1)
class CD3DFramework : public CD3DApplication
{
// Шрифт для отображения FPS и данных видеорежима
CD3DFont* m_pStatsFont;
// Массив целых чисел для хранения блочной карты
int m_iTileMap[100];
short m_shTileMapWidth;
short m_shTileMapHeight;
// Буфер для хранения текстур
LPDIRECT3DTEXTURE9 m_pTexture[32];
// Размеры окна
short m_shWindowWidth;
short m_shWindowHeight;
// Буфер для хранения вершин
LPDIRECT3DVERTEXBUFFER9 m_pVBTile;
protected:
HRESULT OneTimeSceneInit();
HRESULT InitDeviceObjects();
HRESULT RestoreDeviceObjects();
HRESULT InvalidateDeviceObjects();
HRESULT DeleteDeviceObjects();
HRESULT Render();
HRESULT FinalCleanup();
HRESULT CreateD3DXTextMesh(LPD3DXMESH* ppMesh, TCHAR* pstrFont, DWORD dwSize);
// Создание буфера вершин блока
void vInitTileVB(void);
// Рисование блока на экране
void vDrawTile(float fXPos, float fYPos, float fXSize, float fYSize, int iTexture);
public:
LRESULT MsgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
CD3DFramework();
};
Я не собираюсь докучать вам обсуждением кода, который не является специфичным для приложения, поэтому мы сразу перескочим к следующему фрагменту:
struct TILEVERTEX
{
D3DXVECTOR3 position; // Позиция
D3DXVECTOR3 vecNorm; // Нормаль
FLOAT tu, tv; // Координаты текстуры (U,V)
};
Структура данных вершины содержит информацию о местоположении, нормали и координатах текстуры. Это все, что необходимо для отображения на экране графики, которая будет выглядеть двухмерной.
К заслуживающим упоминания переменным класса относятся m_iTileMap, m_shTileMapWidth, m_shTileMapHeight, m_pTexture и m_pVBTile. Массив m_iTileMap хранит информацию блочной карты. Я объявляю массив из 100 целых чисел, поскольку ширина и высота карты будут равны 10 блокам.
Переменные m_shTileMapWidth и m_shTileMapHeight хранят размеры блочной карты. В конструкторе класса я присваиваю этим переменным значение 10.
Массив m_pTexture содержит указатели на текстуры, используемые библиотекой визуализации. Каждое значение в массиве, хранящем блочную карту, является индексом для этого массива.
Указатель m_pVBTile указывает на буфер вершин, необходимый программе для визуализации блоков.
Следующий блок кода, который мы рассмотрим более подробно, выглядит так:
// Создание буфера вершин блока
void vInitTileVB(void);
// Рисование блока на экране
void vDrawTile(float fXPos, float fYPos, float fXSize, float fYSize, int iTexture);
Эти два прототипа функций являются сердцем рассмартиваемого примера, поскольку именно они относятся к отображению блоков. Первая функция, vInitTileVB(), инициализирует буфер вершин блока, используемый для визуализации. Следующая функция, vDrawTile(), применяется для отображения блока на экране.
Файл программы Main.cpp
Файл main.cpp не слишком сложен, поскольку он по большей части следует каркасу приложения DirectX. Первая представляющая для нас интерес функция — это конструктор класса. Вот его код:
CD3DFramework::CD3DFramework()
{
m_strWindowTitle = _T("2D Tile Example");
m_pStatsFont = NULL;
m_shWindowWidth = 480;
m_shWindowHeight = 480;
m_shTileMapWidth = 10;
m_shTileMapHeight = 10;
}
Как видно из приведенного фрагмента кода, в конструкторе класса задается размер блочной карты. Я установил высоту и ширину карты равной 10 блокам, чтобы при выводе карта занимала все окно целиком. Ширина и высота блока равны 48 пикселам, поэтому размер окна устанавливается равным 480 на 480 точек. Поскольку 10 * 48 = 480, данный размер как раз обеспечивает точное совпадение окна и выводимой карты.
Следующий фрагмент кода, который представляет интерес, выполняет инициализацию блочной карты.
HRESULT CD3DFramework::OneTimeSceneInit()
{
m_pStatsFont = new CD3DFont(_T("Arial"), 8, NULL);
if(m_pStatsFont == NULL)
return E_FAIL;
// Заполнение карты блоками с изображением травы
memset(m_iTileMap, 0, (m_shTileMapWidth * m_shTileMapHeight) * sizeof(int));
// заполнение второй половины блоками с изображением песка
for(int i = 0; i < 50; i++) {
m_iTileMap[i+50] = 3;
}
// Случайное размещение камней на траве
// Инициализация генератора случайных чисел
srand(timeGetTime());
for(i = 0; i < 50; i++) {
// Размещение камней на траве, если случайное число = 5
if(rand()%10 == 5)
m_iTileMap[i] = 1;
}
// Размещение переходных блоков между травой и песком
for(i = 50; i < 60; i++) {
m_iTileMap[i] = 2;
}
return S_OK;
}
Изучать этот код мы начнем со строки в которой находится вызов функции memset(). Данный фрагмент очищает блочную карту, присваивая всем ее элементам значение 0. Текстура с номером 0 содержит изображение травы, так что рассматриваемый код заполняет травой всю карту.
Следующая часть кода представляет собой цикл, в котором блокам в нижней половине карты присваивается значение 3. Блок с номером 3 содержит текстуру с изображением песка; таким образом этот код формирует песчанный пляж в нижней части карты.
Идущий далее кусок кода случайным образом размещает блоки с изображением камней в верхней половине карты. Блоки с изображением камней служат только для украшения, поэтому их местоположение не является важным. Чтобы задать плотность этих блоков на карте я использую функцию rand().
И, наконец, код размещает в середине карты переходные блоки, объединяющие области травы и песка. Это обеспецивает плавный и приятно выглядящий переход от блоков с изображением травы к песчанному пляжу.
Поэкспериментируйте с расстановкой различных блоков в коде инициализации. Вы можете попробовать реализовать шаблоны или какие-то другие технологии. Это ваш шанс установить значение блока и увидеть результат.
Теперь я перескочу вниз к фрагменту функции RestoreDeviceObjects(). Это очень важная часть программы, поскольку она содержит код инициализации текстур. Вот как выглядит отвечающий за это фрагмент:
sprintf(szFileName, "grass00.bmp");
if(FAILED(D3DXCreateTextureFromFile(m_pd3dDevice, szFileName, &m_pTexture[0]))) {
return S_OK;
}
sprintf(szFileName, "grass_rocks.bmp");
if(FAILED(D3DXCreateTextureFromFile(m_pd3dDevice, szFileName, &m_pTexture[1]))) {
return S_OK;
}
sprintf(szFileName, "grass_edging_bottom.bmp");
if(FAILED(D3DXCreateTextureFromFile(m_pd3dDevice, szFileName, &m_pTexture[2]))) {
return S_OK;
}
sprintf(szFileName, "beach.bmp");
if(FAILED(D3DXCreateTextureFromFile(m_pd3dDevice, szFileName, &m_pTexture[3]))) {
return S_OK;
}
Код инициализации текстур использует вспомогательную функцию DirectX с именем D3DXCreateTextureFromFile(). Это действительно крутая функция, ведь она содержит весь код, необходимый для загрузки изображений таких форматов, как BMP, TGA и JPEG. Чтобы использовать ее необходимо включить в проект библиотеку d3dx9.lib и заголовочный файл d3dx9tex.h. Прототип функции выглядит следующим образом:
HRESULT D3DXCreateTextureFromFile(
LPDIRECT3DDEVICE9 pDevice,
LPCSTR pSrcFile,
LPDIRECT3DTEXTURE9* ppTexture
);
Первый параметр, pDevice, должен быть указателем на устройство Direct3D, которое вы используете для визуализации. В коде рассматриваемого примера указателем на устройство является переменная m_pd3dDevice. Ее мы и указываем в первом параметре.
Во втором параметре, pSrcFile, ожидается имя загружаемого файла с текстурой. Данный параметр не должен вызывать какие-либо затруднения, поскольку вас достаточно указать заключенное в кавычки имя требуемого файла. Явно указывать путь к файлу не требуется, поскольку при его отсутствии функция пытается найти файл с текстурой в рабочем каталоге приложения. Если необходимо указать другой каталог, можно для получения пути прочитать параметр из реестра. Лично я просто использую подкаталоги в папке с основной программой. Данный метод позволяет использовать несколько каталогов не тревожа параметры в реестре.
Последний параметр, ppTexture, является указателем на текстуру. Если вы вернетесь назад, к моему описанию заголовочного файла, то вспомните, что для хранения указателей на текстуры я использую массив m_pTexture. В этом параметре я также указываю индекс в массиве текстур. Например, для текстуры с номером 1 я указываю в параметре m_pTexture[0], для текстуры с номером 2 использую m_pTexture[1], и так далее.
В завершающей части процедуры инициализации текстур выполняется вызов функции vInitTileVB(). Эта функция всего лишь инициализирует виртуальный буфер для хранения информации о трехмерных блоках.
Вернемся назад и перейдем к коду функции Render(). Вот где происходит волшебство. Пожалуйста, не обращайте внимание на человека за занавесом! Вот как выглядит код, отвечающий за логику визуализации:
HRESULT CD3DFramework::Render()
{
D3DXMATRIX matTranslation;
D3DXMATRIX matRotation;
D3DXMATRIX matRotation2;
D3DXMATRIX matScale;
int iX;
int iY;
int iCurTile;
float fTileX, fTileY;
// Очистка порта просмотра
m_pd3dDevice->Clear(0L, NULL, D3DCLEAR_TARGET,
D3DCOLOR_XRGB(120,120,120),
1.0f, 0L);
// Начало создания сцены
if(SUCCEEDED(m_pd3dDevice->BeginScene())) {
// Вертикаль
for(iY = 0; iY < m_shTileMapHeight; iY++) {
// Горизонталь
for(iX = 0; iX < m_shTileMapWidth; iX++) {
// Вычисление номера отображаемого блока
iCurTile = m_iTileMap[iX + (iY * m_shTileMapWidth)];
// Вычисление экранных координат
fTileX = -240.0f + (iX * 48.0f);
fTileY = 192.0f - (iY * 48.0f);
// Отображение блока
vDrawTile(fTileX, fTileY, 48.0f, 48.0f, iCurTile);
}
}
// Отображение частоты кадров
m_pStatsFont->DrawText(2, 0, D3DCOLOR_ARGB(255,255,255,0), m_strFrameStats);
// Отображение сведений о видеокарте
m_pStatsFont->DrawText(2, 20, D3DCOLOR_ARGB(255,255,255,0), m_strDeviceStats);
// Завершение создания сцены
m_pd3dDevice->EndScene();
}
return S_OK;
}
В первой строке кода вызывается функция Clear(). Она относится к устройству Direct3D и используется для очнстки поверхности визуализации трехмерной графики. Я применяю заливку буфера серым цветом средней интенсивности. Вы можете выбрать тот цвет, который вам больше нравится; он не имеет значения, поскольку видимая область будет полностью заполнена блоками.
Далее расположен вызов функции BeginScene(). Она запускает механизм трехмерной визуализации. Вы должны вызвать эту функцию перед выполнением операций трехмерной графики.
Теперь начинается самая интересная часть. Следующий фрагмент кода является циклом визуализации, необходимым для отображения блоков. В нем содержатся два вложенных цикла; внешний перебирает строки карты сверху вниз, а внутренний цикл перебирает блоки в строке слева направо. Комбинация этих двух циклов позволяет перебрать все блоки отображаемого фрагмента карты.
В первой строке кода внутреннего цикла осуществляется вычисление номера отображаемого блока. Полученное значение сохраняется в переменной iCurTile. Позиция отображаемого блока в массиве вычисляется путем сложения значения счетчика внутреннего цикла со значением счетчика внешнего цикла, умноженным на ширину карты. Возможно, сейчас вы вспомнили приведенную ранее в главе формулу. Получив значение блока, вы знаете, какую текстуру следует использовать при его отображении.
Следующие инструкции вычисляют, в каком месте экрана должен размещаться отображаемый блок. Поскольку в программе используется трехмерная графика, для задания смещения блока применяются значения с плавающей точкой. Размеры создаваемого программой окна составляют 480 точек в ширину и в высоту. Учитывая эту особенность, для того, чтобы блоки отображались вплотную к границе окна, они должны смещаться на 240 единиц влево. Аналогичным образом для того чтобы блоки отображались вплотную к верхней границе окна, они должны смещаться на 240.0 – 48.0, или 192.0 единицы вверх от начала координат.
В следующей строке кода вызывается написанная мной функция vDrawTile(). Прототип функции выглядит следующим образом:
vDrawTile(
float fXPos,
float fYPos,
float fXSize,
float fYSize,
int iTexture)
Первый параметр, fXPos, содержит координату по оси Х того места на экране, где будет нарисован блок. Это значение с плавающей точкой, определяющее местоположение вдоль оси X в трехмерной системе координат. Не путайте его с координатой точки на экране.
Следующий параметр, fYPos, аналогичен первому, за исключением того, что задает местоположение в трехмерной системе координат вдоль оси Y.
Идущие далее два параметра, fXSize и fYSize, устанавливают размер отображаемого на экране блока. Вы можете задать любой желаемый размер, поскольку функция в случае необходимости выполняет соответствующее масштабирование блоков. В нашем примере оба размера блока равны 48.0 единицам.
Последний параметр является индексом в массиве текстур m_pTexture. Он задает текстуру для визуализации.
В результате вызова функции vDrawTile() блок появляется в экранном буфере. Все что осталось сделать — вывести частоту кадров и сведения о видеокарте, а затем отобразить готовую сцену. Этим и занимается оставшаяся часть кода функции визуализации.