netlib.narod.ru< Назад | Оглавление | Далее >

Отображение блоков

Мы уже там? Мы уже там? Мы уже там? ДА!!!! Мы уже там. Извините, мне просто вспомнилась последняя поездка на автомобиле, в которую я взял своих детей.

Правильно, вы наконец добрались до самой сути визуализации блоков. Здесь я рассмотрю реальный код, предназначенный для реализации следующих сценариев:

Отображение двухмерных блоков

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


Рис. 5.38. Окно программы, демонстрирующей визуализацию двухмерных блоков

Рис. 5.38. Окно программы, демонстрирующей визуализацию двухмерных блоков


Выглядит не слишком захватывающе? Хорошо, я понимаю, что вы уже видели десятки блочных карт, но для этой в сопроводительных файлах к книге (www.wordware.com/files/games) доступен полный исходный код, создающий ее! Загрузите проект D3DFrame_2DTiles, чтобы двигаться дальше.

Архитектура проекта D3DFrame_2DTiles

Проект содержит два уникальных файла: main.cpp и main.h. Остальные включенные в проект файлы, — d3dapp.cpp, d3denumeration.cpp, d3dfont.cpp, d3dsettings.cpp, d3dutil.cpp и dxutil.cpp, — являются частью Microsoft DirectX 9.0 SDK.

Уникальные файлы я создал специально для этого примера. Вы можете обратить внимание на префикс D3DFrame у названия проекта. Он означает, что при создании программы я пользовался предоставляемым Microsoft каркасом приложения Direct3D. Каждый раз, когда вы увидите этот префикс, знайте, что я использовал каркас приложения. Если вам не нравятся подобные каркасы, не волнуйтесь, — я покажу как выполнить ту же самую работу не применяя их.

Структура проекта показана на рис. 5.39.


Рис. 5.39. Структура файлов программы, демонстрирующей двухмерную блочную графику

Рис. 5.39. Структура файлов программы, демонстрирующей двухмерную блочную графику


Как видно на рисунке, проект включает собственные уникальные файлы, файлы каркаса приложения DirectX и библиотеки d3d9.lib, dxguid.lib, d3dx9dt.lib, d3dxof.lib, comctl32.lib и winmm.lib.

Заголовочный файл Main.h

Есть только один заголовочный файл, который следует обсудить подробно - файл main.h. Он содержит всю заголовочную информацию, необходимую для программы. Вот как выглядит содержащийся в этом файле код:

#define STRICT
#include <windows.h>
#include <commctrl.h>
#include <commdlg.h>
#include <math.h>
#include <tchar.h>
#include <stdio.h>
#include <D3DX9.h>
#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)
};

 

ПРИМЕЧАНИЕ
Действительно, в названии программы присутствует слово 2D, но не позволяйте ему одурачить вас. DirectX больше не работает с чисто двухмерной графикой, так что теперь программы используют трехмерную графику, которая выглядит как двухмерная. По этой причине я создал структуру для описания вершин, содержащую информацию, необходимую для создания трехмерного блока, который будет выглядеть как имеющий только два измерения. Если вы хотите получить больше информации об этом аспекте программирования, я рекомендую вам обратиться к главе 6, которая сосредотачивается на программировании двухмерной графики в трехмерном окружении.

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

К заслуживающим упоминания переменным класса относятся 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(). Она запускает механизм трехмерной визуализации. Вы должны вызвать эту функцию перед выполнением операций трехмерной графики.

ВНИМАНИЕ
Вы должны начинать визуализацию с вызова функции BeginScene() и завершать ее вызовом функции EndScene(). Если вы не сделаете это, ваша программа аварийно завершится.

Теперь начинается самая интересная часть. Следующий фрагмент кода является циклом визуализации, необходимым для отображения блоков. В нем содержатся два вложенных цикла; внешний перебирает строки карты сверху вниз, а внутренний цикл перебирает блоки в строке слева направо. Комбинация этих двух циклов позволяет перебрать все блоки отображаемого фрагмента карты.

В первой строке кода внутреннего цикла осуществляется вычисление номера отображаемого блока. Полученное значение сохраняется в переменной 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() блок появляется в экранном буфере. Все что осталось сделать — вывести частоту кадров и сведения о видеокарте, а затем отобразить готовую сцену. Этим и занимается оставшаяся часть кода функции визуализации.

Смотрите, это было не так уж и плохо, правда? Поток выполнения программы показан на рис. 5.40.


Рис. 5.40. Поток выполнения программы, отображающей двухмерные блоки

Рис. 5.40. Поток выполнения программы, отображающей двухмерные блоки


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

Отображение двухмерных изометрических блоков

Отображение изометрических блоков является очень сложной темой. Была опубликована целая книга, посвященная созданию изометрических игр. Код, который я буду рассматривать, не охватывает все возможные ситуации, могущие возникнуть при визуализации изометрических блоков. Он скорее является основанием для вашей собственной разработки. Я расскажу о некоторых проблемах и способах их решения, но многие вопросы очень обширны и выходят за рамки книги. Я предлагаю вам познакомиться с приведенным здесь кодом, а затем использовать его в собственных исследованиях и разработках. На рис. 5.41 показан результат работы программы, отображающей двухмерные изометрические блоки.


Рис. 5.41. Окно программы, демонстрирующей визуализацию двухмерных изометрических блоков

Рис. 5.41. Окно программы, демонстрирующей визуализацию двухмерных изометрических блоков


Проект, содержащий код программы называется D3DFrame_Isometric2DTiles. Вы найдете его в сопроводительных файлах (www.wordware.com/files/games). Загрузите его и следуйте за мной дальше.

Архитектура проекта D3DFrame_Isometric2DTiles

Проект содержит два уникальных файла: main.cpp и main.h. Остальные включенные в проект файлы, — d3dapp.cpp, d3denumeration.cpp, d3dfont.cpp, d3dsettings.cpp, d3dutil.cpp и dxutil.cpp, — являются частью Microsoft DirectX 9.0 SDK. Звучит знакомо, а? Вы должны заметить повторяющуюся тему в программах, написанных с использованием каркаса приложения Direct3D. Я пытаюсь оставить вещи настолько простыми, насколько это возможно, и буду так же поступать в дальнейшем.

Заголовочный файл Main.h

И снова наше внимание будет сосредоточено только на одном заголовочном файле — main.h. Раньше вы уже видели большую часть содержимого этого файла, поэтому я сконцентрируюсь на наиболее важных фрагментах. Первый из таких фрагментов — объявление переменных класса:

// Массив целых чисел для хранения карты
int                  m_iTileMap[100][2];
short                m_shTileMapWidth;
short                m_shTileMapHeight;
// Буфер для хранения текстур
LPDIRECT3DTEXTURE9   m_pTexture[32];

Ну что, посмотрели? Все переменные остались теми же самыми, за исключением еще одного измерения, добавленного к массиву m_iTileMap. Массив был изменен, чтобы хранить несколько слоев, для чего к нему было добавлено еще одно измерение. Новый слой, добавленный в этом примере, предназначен для того, чтобы хранить детали блочной графики. Вместо того, чтобы хранить все в одном слое, мы теперь воспользуемся двумя.

Вот и все, что я хотел сказать о заголовочном файле; остальные его части ничем не отличаются от тех, которые мы разобрали, обсуждая отображение двухмерных блоков.

Файл программы Main.cpp

Загрузите файл main.cpp. Первая вещь, которая представляет для нас интерес — глобальная переменная с именем g_iNumTextures. Она содержит количество загружаемых в память текстур блоков. Я создал ее, чтобы упростить добавление блоков в программе. В настоящее время значение этой переменной равно 9. Если вы будете экспериментировать с программой и добавите свои собственные блоки, убедитесь, что соответствующим образом изменено и значение переменной.

Переместимся далее, к коду конструктора класса. Вы видите, что в нем присваиваются значения нескольким переменным класса:

m_shWindowWidth   = 640;
m_shWindowHeight  = 320;
m_shTileMapWidth  = 10;
m_shTileMapHeight = 10;

На этот раз я создаю окно, ширина которого равна 640 точкам, а высота — 320 точкам. Я поступаю так по той причине, что визуализация изометрических блоков слегка отличается от визуализации двухмерных квадратных блоков, и для нее требуется большая экранная область.

Следующий блок переменных задает размеры визуализируемой блочной карты. Если вы решите увеличить размер блочной карты, не забудьте добавить элементы к массиву m_iTileMap.

Теперь взглянем на функцию класса OneTimeSceneInit(). В этой функции я заполняю два слоя блочной карты данными, после того, как очищу массив функцией memset().

// Инициализация генератора случайных чисел
srand(timeGetTime());
for(int i = 0; i < 100; i++) {
   // Заполнение базового слоя блоками с изображением травы песка и брусчатки
   if(rand()%10 == 3)
      m_iTileMap[i][0] = 2;
   else if(rand()%10 == 4)
      m_iTileMap[i][0] = 3;
   else
      m_iTileMap[i][0] = 4;
   // Заполнение слоя деталей деревьями и колоннами
   if(rand()%10 == 5)
      m_iTileMap[i][1] = 6;
   else if(rand()%10 == 4)
      m_iTileMap[i][1] = 5;
   else if(rand()%10 == 3)
      m_iTileMap[i][1] = 8;
}

Сперва вызов функции srand() инициализирует генератор случайных чисел, используя в качестве начального значения текущее системное время. Чтобы получить текущее значение времени, я вызываю функцию timeGetTime(). Она находится в библиотеке winmm.lib и требует, чтобы в программу был включен заголовочный файл mmsystem.h. Инициализация генератора случайных чисел позволяет получать различные результаты при каждом запуске программы.

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

Обратите внимание, что устанавливая блоки основного слоя я обращаюсь к первому слою массива карты, используя ссылки вида m_iTileMap[xxTileToChangexx] [0]. Располагая блоки с деталями, я обращаюсь ко второму слою, и ссылки выглядят так: m_iTileMap[xxTileToChangexx] [1]. Если вы желаете, то можете добавить еще измерения; только не забудьте проверить, что вы заполняете эти слои осмысленными значениями.

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

Прииигооотооовииитьсяяя к вииизуууааалииизааацииииии!!!!! (Мои извинения Майклу Бафферу). Сейчас действительно настало время перейти к отображению блоков, так что не будем задерживаться...

// Вертикаль
for(iY = 0; iY < m_shTileMapHeight; iY++) {
   // Горизонталь
   for(iX = 0; iX < m_shTileMapWidth; iX++) {
      //---------------------------------------------
      //        ВИЗУАЛИЗАЦИЯ БАЗОВОГО СЛОЯ
      //---------------------------------------------
      // Вычисление номера отображаемого блока
   iCurTile = m_iTileMap[iX + (iY * m_shTileMapWidth)][0];

      // Вычисление экранных координат
      fTileX = -32.0f + (iX*32.0f) - (iY*32.0f);
      fTileY = 128.0f - ((iY*16.0f) + (iX*16.0f));
      // Отображение блока
      vDrawTile(fTileX, fTileY, 64.0f, 32.0f, iCurTile);

      //---------------------------------------------
      //        ВИЗУАЛИЗАЦИЯ СЛОЯ С ДЕТАЛЯМИ
      //---------------------------------------------
      // Вычисление номера отображаемого блока
      iCurTile = m_iTileMap[iX + (iY * m_shTileMapWidth)][1];
      if(iCurTile != 0) {
         // Вычисление экранных координат
         fTileX = -32.0f + (iX*32.0f) - (iY*32.0f);
         fTileY = 128.0f - ((iY*16.0f) + (iX*16.0f));
         if(iCurTile == 5)
            vDrawTile(fTileX, fTileY, 64.0f, 125.0f, iCurTile);
         else if(iCurTile == 6)
            vDrawTile(fTileX, fTileY, 67.0f, 109.0f, iCurTile);
         else if(iCurTile == 8)
            vDrawTile(fTileX, fTileY, 64.0f, 64.0f, iCurTile);
      }
   }
}

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

Начальный фрагмент кода визуализации занимается отображением базового слоя. Он выводит на экран блоки, изображающие траву, брусчатку и песок. Возможно вы заметитли переменную iCurTile. Я использую ее, чтобы вычислить, какой блок должен быть отображен. Работает этот код точно также, как и в предыдущей программе, за исключением добавленной ссылки на дополнительное измерение массива карты. В данном случае я ссылаюсь на первое измерение буфера блоков, или в терминах кода — [0].

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

Позиция по горизонтали вычисляется путем вычитания из смещения экранной области суммы значения счетчика внешнего цикла, умноженного на половину ширины блока и значения счетчика внутреннего цикла, умноженного на половину ширины блока. Вот соответствующая формула:

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

Следующим и последним важным фрагментом кода визуализации является отображение слоя блочной карты с деталями ландшафта. В нем есть только одна заслуживающая внимания особенность — я проверяю какой блок отображается и изменяю размеры области визуализации согласно размерам блока. Это необходимо не только потому, что блоки с деталями ландшафта располагаются поверх другого слоя, но и потому, что размеры этих блоков различны! Поскольку габариты блоков отличаются, для того чтобы они отображались правильно, я должен изменять размеры области визуализации. Если хотите посмотреть, что получится без такой корректировки, не стесняйтесь и удалите код, изменяющий размеры отображаемых блоков.

Вот мы и обсудили отображение изометрических блоков. А вы думали, что на это потребуется целая книга! Ох, я совсем забыл еще об одном примере программы. Уберите шампанское — нам надо посмотреть еше один вариант кода для отображения изометрических блоков.

Отображение двухмерных изометрических блоков со спрайтами

В этой программе используется предусмотренный в DirectX 9.0 SDK интерфейс ID3DXSprite. Он упрощает процесс рисования на экране двухмерных изображений. Я заменил большую часть используемых в предыдущей программе вызовов функкций, относящихся к трехмерной графике, вызовами функций работы со спрайтами. Первое, что бросается в глаза — код стал проще и яснее. Хорошо это или плохо — решать вам. На рис. 5.42 показан результат работы программы, отображающей двухмерные изометрические блоки с использованием спрайтов.


Рис. 5.42. Окно программы, демонстрирующей визуализацию двухмерных изометрических блоков с использованием спрайтов

Рис. 5.42. Окно программы, демонстрирующей визуализацию двухмерных изометрических блоков с использованием спрайтов


Проект, содержащий код программы называется D3DFrame_Isometric2DSpriteTiles. Вы найдете его в сопроводительных файлах. Загрузите его и следуйте дальше.

Архитектура проекта D3DFrame_Isometric2DSpriteTiles

Проект содержит два уникальных файла: main.cpp и main.h. Остальные включенные в проект файлы, — d3dapp.cpp, d3denumeration.cpp, d3dfont.cpp, d3dsettings.cpp, d3dutil.cpp и dxutil.cpp, — являются частью Microsoft DirectX 9.0 SDK. Снова используется каркас приложения DirectX.

Заголовочный файл Main.h

В заголовочный файл было внесено несколько изменений. Так я удалил код, относящийся к буферу вершин блока, поскольку в данном примере этот буфер нам не потребуется. Вместо функции vInitTileVB() и переменных класса мы будем использовать единственный указатель LPD3DXSPRITE. Вместо функции vDrawTile() мы воспользуемся функцией BltSprite(). Ее прототип выглядит так:

HRESULT BltSprite(
   RECT               *pDestRect,
   LPDIRECT3DTEXTURE9 pSrcTexture,
   RECT               *pSrcRect)

Первый параметр, pDestRect, является указателем на переменную типа RECT. Этот прямоугольник указывает функции визуализации в каком месте экрана следует отображать текстуру.

Следующий параметр называется pSrcTexture; это указатель на текстуру, которая будет выведена на экран.

Третий параметр, pSrcRect, является указателем на еще одну переменную типа RECT. Он сообщает функции визуализации, какой фрагмент текстуры должен быть отображен. Это позволяет отображать как отдельные части текстуры, так и всю ее целиком.

Файл программы Main.cpp

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

D3DXCreateSprite(m_pd3dDevice, &pd3dxSprite);

Просто, да? Функция D3DXCreateSprite() выполнит за вас всю работу, необходимую чтобы создать спрайтовое устройство. Ей передаются два параметра. Первый параметр — это указатель на устройство трехмерной визуализации. Второй параметр является адресом указателя на спрайтовое устройство, которое будет создано функцией.

Переместимся к функции Render(), чтобы посмотреть следующий набор изменений кода. Цикл визуализации выглядит также как и раньше — один внешний цикл и один внутренний. Самые значительные отличия расположены в коде внутренннего цикла. Вместо вызова функции vDrawTile() теперь используется вызов функции BltSprite(). Еще одно отличие заключается в том, что для указания местоположения блока теперь используются прямоугольники, а не значения с плавающей точкой, определяющие координаты в трехмерном пространстве. Отметим важный момент — прямоугольники определяют местоположение на экране, а не в трехмерном пространстве.

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

Перед тем, как начать визуализацию, вы должны вызвать функцию спрайтового устройства Begin(). Завершать визуализацию следует вызовом функции спрайтового устройства End(). Эти вызовы полностью отличаются от того, что мы делали при работе с трехмерным устройством. Они нужны только в том случае, если вы используете спрайты, и не применяются, когда используется трехмерная визуализация.

Зачем использовать спрайты?

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

Отображение трехмерных блоков

Отображение трехмерных блоков! Оооо, звучит угрожающе, не так ли? Хотя название темы звучит пугающе, в действительности она почти ничем не отличается от рассмотренного в этой главе ранее отображения двухмерных блоков. Действительно, в каждом, использующем двухмерную графику примере из этой главы, применялась трехмерная визуализация. Главное видимое отличие заключается в том, что «настоящие» трехмерные программы не используют ортогональную проекцию. На рис. 5.43 показан результат работы программы, демонстрирующей отображение трехмерных блоков.


Рис. 5.43. Окно программы, демонстрирующей визуализацию трехмерных блоков

Рис. 5.43. Окно программы, демонстрирующей визуализацию трехмерных блоков


Проект, содержащий код программы называется D3DFrame_3DTiles. Вы найдете его в сопроводительных файлах. Загрузите его и следуйте дальше.

Архитектура проекта D3DFrame_3DTiles

Проект содержит два уникальных файла: main.cpp и main.h. Остальные включенные в проект файлы, — d3dapp.cpp, d3denumeration.cpp, d3dfile.cpp, d3dfont.cpp, d3dsettings.cpp, d3dutil.cpp и dxutil.cpp, — являются частью Microsoft DirectX 9.0 SDK.

Обратите внимание, что название одного из файлов выделено полужирным шрифтом. Файл d3dfile.cpp отсутствовал ранее и был добавлен только к этому проекту. Его назначение — помогать загружать файлы формата .x, содержащие информацию о трехмерных объектах. Он необходим для загрузки трехмерных моделей, созданных в таких пакетах визуального моделирования, как Maya, 3D Studio MAX и MilkShape. Если хотите, вы можете написать собственный загрузчик моделей, но встроенные функции, предоставляемые файлом d3dfile.cpp сделают вашу жизнь намного проще.

Заголовочный файл Main.h

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

#define STRICT
#include <windows.h>
#include <commctrl.h>
#include <commdlg.h>
#include <math.h>
#include <tchar.h>
#include <stdio.h>
#include <D3DX9.h>
#include "DXUtil.h"
#include "D3DEnumeration.h"
#include "D3DSettings.h"
#include "D3DApp.h"
#include "D3DFont.h"
#include "D3DFile.h"
#include "D3DUtil.h"

int g_iNumTiles = 2;

// Формат вершин для трехмерных блоков
struct D3DVERTEX
{
    D3DXVECTOR3 p;
    D3DXVECTOR3 n;
    FLOAT       tu, tv;

    static const DWORD FVF;
};
const DWORD D3DVERTEX::FVF = D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1;

class CD3DFramework : public CD3DApplication
     {
   CD3DFont*    m_pStatsFont;
   TCHAR        m_strFont[LF_FACESIZE];
   DWORD        m_dwFontSize;
   // Данные трехмерных объектов
   CD3DMesh*    m_pObject[32];
   // Массив целых чисел для хранения блочной карты
   int          m_iTileMap[100];
   short        m_shTileMapWidth;
   short        m_shTileMapHeight;
   // Размеры окна
   short        m_shWindowWidth;
   short        m_shWindowHeight;

protected:
   HRESULT OneTimeSceneInit();
   HRESULT InitDeviceObjects();
   HRESULT RestoreDeviceObjects();
   HRESULT InvalidateDeviceObjects();
   HRESULT DeleteDeviceObjects();
   HRESULT Render();
   HRESULT FrameMove();
   HRESULT FinalCleanup();
   HRESULT CreateD3DXTextMesh(LPD3DXMESH* ppMesh, TCHAR* pstrFont, DWORD dwSize);

public:
   LRESULT MsgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
   CD3DFramework();
};

Первое изменение находится в разделе включаемых файлов. Файлу d3dfile.cpp требуется заголовочный файл d3dfile.h. Поскольку наша программа использует функции из файла d3dfile.cpp, заголовочный файл должен быть также включен в исходный код.

ПРИМЕЧАНИЕ
Вспомогательные файлы, такие как d3dfile.cpp, d3dapp.cpp, d3dsettings.cpp, и т.д., находятся в той папке, куда вы установили DirectX SDK. Если вы не меняли предлагаемый путь по умолчанию, файлы находятся в папке C:\DXSDK\Samples\C++\Common.

Следующий фрагмент, который представляет интерес — объявление переменной g_iNumTiles расположенное сразу после перечисления включаемых файлов. Данная переменная содержит количество загружаемых программой трехмерных моделей. Очень важно отслеживать это число, поскольку программа должна знать, сколькими моделями ей управлять. Если вы решите добавить к рассматриваемому примеру еще модели, убедитесь, что значение переменной также соответствующим образом увеличено.

Следующее изменение находится в структуре данных D3DVERTEX. Ее формат слегка отличается от использованного ранее. Это сделано для поддержки формата вершин трехмерных моделей. Для такой поддержки необходима переменная FVF типа DWORD. Это единственное отличие от использованного ранее формата вершин.

Спустимся ниже к определению класса. Здесь добавлена новая переменная с именем m_pObject. Это массив значений типа CD3DMesh. Объект CD3DMesh предоставляется библиотекой d3dfile.cpp. Это ваше окно в мир загрузки и отображения трехмерных моделей. Я произвольным образом установил размер массива равным 32. Это указывает, что максимальное количество загруженных блоков будет равно 32. Не стоит волноваться, — если захотите, вы в дальнейшем всегда сможете изменить это число.

Файл программы Main.cpp

Теперь откройте файл main.cpp, чтобы увидеть код, используемый в данном примере. Ниже приведен первый фрагмент кода, который представляет для нас интерес:

HRESULT CD3DFramework::OneTimeSceneInit()
{
   int i;
   m_pStatsFont = new CD3DFont(_T("Arial"), 8, NULL);
   if(m_pStatsFont == NULL)
      return E_FAIL;
   // Выделение памяти для блоков
   for(i = 0; i < g_iNumTiles; i++) {
      m_pObject[i] = new CD3DMesh();
   }
   // Заполнение карты блоками с кодом 0
   memset(m_iTileMap, 0, (m_shTileMapWidth * m_shTileMapHeight) * sizeof(int));
   // Случайное размещение скал на траве
   // Инициализация генератора случайных чисел
   srand(timeGetTime());
   for(i = 0; i < 100; i++) {
      if(rand() % 5 == 3)
         m_iTileMap[i] = 1;
      else
         m_iTileMap[i] = 0;
   }
   return S_OK;
}

Новые действия начинаются в первом цикле for. В нем выделяется память для трехмерных объектов (блоков). Для этой цели используется оператор new. Смотрите, разве это не просто?

Далее расположен еще один цикл for предназначенный для случайного размещения блоков на карте. В данном примере используются всего два типа блоков, так что код должен всего лишь выбрать одни из этих двух блоков и поместить его на карту. В данном случае 1 — это блок с изображением горы, а 0 — блок с изображением травы.

Загрузка трехмерных моделей

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

HRESULT CD3DFramework::InitDeviceObjects()
{
   HRESULT hr;
   char szFileName[512];
   // Инициализация шрифта
   if(FAILED(hr = m_pStatsFont->InitDeviceObjects(m_pd3dDevice)))
      return hr;
   //  Загрузка информации трехмерных блоков
   for(int i = 0; i < g_iNumTiles; i++) {
      // Создаем имя файла
      sprintf(szFileName, "ground_tile%d.x", i+1);
      // Загружаем сетку
      if(FAILED(m_pObject[i]->Create(m_pd3dDevice, _T(szFileName))))
         return D3DAPPERR_MEDIANOTFOUND;
      // Устанавливаем тип вершин
      m_pObject[i]->SetFVF(m_pd3dDevice, D3DVERTEX::FVF);
   }
   return S_OK;
}

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

HRESULT Create(
   LPDIRECT3DDEVICE9 pd3dDevice,
   TCHAR* strFilename)

Первый параметр, pd3dDevice, является указателем на используемое в приложении трехмерное устройство. Я передаю в этом параметре указатель m_pd3dDevice, который инициализируется в коде каркаса приложения.

Следующий параметр, strFilename, является именем файла, содержащего загружаемый в память объект. Здесь я передаю созданное ранее имя файла.

Теперь, когда объект загружен в память, укажем формат вершин для объекта. Выполняйте эти действия когда хотите управлять форматом вершин, используемым при визуализации объекта. Если вам это не нужно, нет никаких причин использовать SetFVF(). Данный вызов добавлен только чтобы обеспечить дополнительный контроль.

Точечный источник света

Переместимся ниже к функции CD3DFramework::RestoreDeviceObjects(), чтобы увидеть сделанные мной незначительные изменения ее кода. Отличий совсем немного; главное из них то, что в этом примере я использую новый тип источника света. Вот код для нового точечного источника света:

ZeroMemory(&d3dLight, sizeof(D3DLIGHT9));
d3dLight.Type         = D3DLIGHT_POINT;
d3dLight.Diffuse.r    = 1.0f;
d3dLight.Diffuse.g    = 1.0f;
d3dLight.Diffuse.b    = 1.0f;
d3dLight.Position.x   = 0.0f;
d3dLight.Position.y   = -20.0f;
d3dLight.Position.z   = 20.0f;
d3dLight.Attenuation0 = 1.0f;
d3dLight.Attenuation1 = 0.0f;
d3dLight.Range        = 100.0f;

В предыдущих примерах программ использовался направленный источник света. DirectGraphics предлагает для использования и другие типы освещения, такие как зональное освещение (прожектор) и точечный источник света. В рассматриваемом примере я применяю точечный источник света.

Значение Type указывает системе визуализации тип источника света. В данном примере я задаю значение D3DLIGHT_POINT. Оно указывает, что система визуализации должна ожидать и использовать параметры для точечного источника света.

Структура Diffuse задает цвет источника света. Она содержит три компонента — красный, зеленый и синий. Значения каждого компонента должны находиться в диапазоне от 0.0 до 1.0. Значение 0.0 соответствует полному отсутствию данного цвета, а значение 1.0 — его максимальной интенсивности. Поскольку для каждого из цветов я указал значение 1.0, в результате будет получен белый свет максимальной интенсивности. Если вы не знакомы с освещением трехмерных сцен, я предлагаю вам поиграть с параметрами, чтобы увидеть, какое влияние они оказывают на сцену.

Структура Position содержит местоположение источника света в трехмерном пространстве. Я разместил его в точке с координатами (0.0, –20.0, 20.0).

Значение Attentuation0 определяет, как интенсивность света будет изменяться с увеличением расстояния. Это значение устанавливает константу, с которой начнется изменение интенсивности. Значение Attenuation1 устанавливает следующую константу, используемую для изменения интенсивности. Задав масштабирование в диапазоне от 1.0 до 0.0, я указываю, что с увеличением расстояния интенсивность света должна уменьшаться.

Значение Range указывает расстояние на котором источник света перестает оказывать влияние на объекты. В нашем примере источник света не освещает предметы, которые удалены от него более, чем на 100 единиц.

Визуализация трехмерных моделей

Готовы ли вы к визуализации трехмерных блоков? Я готов! Вот новый и улучшенный код визуализации трехмерных блоков:

HRESULT CD3DFramework::Render()
{
   D3DXMATRIX   matTranslation;
   int    iX, iY;
   int    iCurTile;
   float  fXPos;
   float  fYPos;

   // Очистка порта просмотра
   m_pd3dDevice->Clear(0L, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
                       D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0L);

   // Начало создания сцены
   if(SUCCEEDED(m_pd3dDevice->BeginScene()))
      {
      for(iY = 0; iY < 10; iY++) {
         // Горизонтали
         for(iX = 0; iX < 10; iX++) {
            // Вычисляем, какой блок отображать
            iCurTile = m_iTileMap[iX + (iY * m_shTileMapWidth)];
            // Вычисляем местоположение блока
            fXPos = (-5.0f * iX) + 22.5f;
            fYPos = (-5.0f * iY) + 32.5f;
            // Устанавливаем позицию блока
            D3DXMatrixTranslation(&matTranslation, fXPos, fYPos, 0.0f);
            m_pd3dDevice->SetTransform(D3DTS_WORLD, &matTranslation);
            // Отображаем блок
            m_pObject[iCurTile]->Render(m_pd3dDevice);
         }
      }

      // Показываем частоту кадров
      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;
}

Обратите внимание, насколько этот код похож на предыдущие примеры. Визуализация трехмерных моделей на самом деле не так уж и сложна. В рассматриваемом примере присутствует обычный набор ключевых фрагментов. Есть внешний цикл для визуализации блоков вдоль оси Y и внутренний цикл для визуализации блоков вдоль оси X. Местоположение блока вычисляется обычным способом с небольшим изменением координат. Главное отличие — вызов функции D3DXMatrixTranslation().

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

ВНИМАНИЕ
Убедитесь, что значение первого параметра функции SetTransform() равно D3DTS_WORLD. В ином случае вы измените что угодно, но только не местоположение объекта!

Теперь объект находится в требуемой позиции, и для его отображения следует вызвать функцию Render(). Функция Render() принадлежит объекту CD3DMesh и выполняет за вас всю необходимую работу. Вам надо только передать указатель на трехмерное устройство, и объект сделает все остальное. Разве не круто?

Теперь вы знаете как отображать трехмерные блоки. Получен ответ на древний вопрос! Я признаю, что еще многое требуется изучить, но вы уже на пути к созданию полностью трехмерной стратегической игры с блочной графикой.


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz