Глава 10 - клас terrain
клас Terrain
У цьому розділі ви створите клас для роботи з ландшафтом з ім'ям Terrain. де спершу створите методи для завантаження карти висот, генерації тривимірної сітки і малювання ландшафту. Надалі ви додасте в цей клас нові методи, використовувані для запиту висоти ландшафту в заданій точці і перевірки зіткнень.
Завантаження карти висот ландшафту
Перший етап генерації ландшафту - читання його даних з карти висот. Оскільки карта висот зберігається як файл RAW. ви можете використовувати FileStream для читання її даних і зберігати їх в масиві byte []. Зауважте, що оскільки у карти висот немає заголовка, вам необхідно знати її розмір, і він повинен відповідати розміру решітки вершин. Для читання і збереження даних карти висот використовуйте наступний код:
У показаному коді ви Новомосковскете і зберігаєте карту висот того ж самого розміру, що й решітка вершин, яку ви збираєтеся створити. Ви визначаєте розмір решітки вершин через змінні vertexCountX і vertexCountZ. є параметрами, використовуваними для завантаження карти висот. Мінлива vertexCountX визначає кількість вершин в рядку сітки (по осі X), а vertexCountZ визначає кількість вершин в стовпці сітки (по осі Z).
Ви зберігаєте дані карти висот у змінній heightmap. що є атрибутом класу Terrain. Зауважте, що дані карти висот вам будуть потрібні пізніше, щоб ви могли запросити висоту певної точки ландшафту. Після читання карти висот ви можете згенерувати сітку ландшафту, використовуючи ці дані. Ви створите метод GenerateTerrainMesh. щоб генерувати сітку ландшафту, складену з індексів і вершин. Метод GenerateTerrainMesh повинен бути викликаний після того, як завантажена карта висот.
Ви можете зберігати перетворення ландшафту (переміщення, обертання і масштабування) всередині класу Terrain. використовуючи створений в розділі 9 клас Transformation. Для цього додайте до класу Terrain новий атрибут типу Transformation і назвіть його transformation. Потім, коли карта висот завантажена, ви повинні створити новий екземпляр Transformation:
І, нарешті, ви повинні завантажити власний ефект для ландшафту і инкапсулировать його в об'єкт TerrainEffect. Як говорилося в главі 8. ви повинні створювати допоміжний клас для кожного створеного вами ефекту, що допоможе вам управляти параметрами ефекту і модифікувати їх. Клас TerrainMaterial - це ще один клас, який ви створюєте для конфігурації ефектів ландшафту:
Власний ефект, який ви створите для ландшафту, забезпечить більш реалістичну візуалізацію з використанням мультитекстурирования і накладення нормалей. Мультитекстурирование дозволяє накладати на одну і ту ж поверхню відразу кілька текстур, а накладення нормалей дозволяє підвищити деталізацію ландшафту без збільшення складності його сітки. Ви створите ефект, який використовується для візуалізації ландшафту, в кінці цієї глави. Нижче показаний код методу Load класу Terrain:
Метод Load отримує в якості параметрів ім'я файлу карти висот; розмір ландшафту, виражений в кількості вершин (по осях X і Z); розмір блоку, який представляє відстань між сусідніми вершинами; і масштабний коефіцієнт висоти, який використовується для масштабування висоти ландшафту. Всі ці параметри, за винятком імені файлу карти висот, зберігаються в класі Terrain а атрибутах vertexCountX. vertexCountZ. blockScale і heightScale. відповідно.
Генерація сітки ландшафту
Щоб згенерувати сітку ландшафту вам треба згенерувати її вершини і індекси. Індекси сітки зберігають порядок, в якому повинні комбінуватися вершини сітки для освіти трикутників. Кожна вершина сітки містить просторові координати і зберігає деякі необхідні для візуалізації атрибути, такі як нормаль і координати текстури. Ви повинні згенерувати індекси сітки перш її вершин, оскільки деякі з атрибутів вершин, такі, як нормаль вершини, ви можете вирахувати тільки якщо знаєте, які вершини використовуються в кожному трикутнику.
Зверніть увагу, що вершини ландшафту зберігаються як масив структур VertexPositionNormalTangentBinormal. Ви повинні створити цю допоміжну структуру для зберігання даних вершин тому що вам необхідно зберігати розташування, координати текстури, нормаль, дотичну і бінормаль кожної вершини, а в XNA немає класу, який зберігав би всі ці атрибути вершини. Ось код для структури VertexPositionNormalTangentBinormal:
У структурі VertexPositionNormalTangentBinormal є всі необхідні для вершини атрибути: місце розташування, координати текстури, нормаль, дотична і бінормаль. У цій структурі також оголошений масив VertexElement. що містить формат даних вершини, де вказані тип і розмір кожного елемента в описі вершини.
Генерація індексів сітки
У цьому розділі ви створите метод GenerateTerrainIndices для генерації індексів сітки ландшафту. Індекси сітки визначають в якому порядку повинні комбінуватися вершини для генерації трикутників. На рис. 10.4 показані індекси вершин в сітці і як вони комбінуються для формування трикутників.

Мал. 10.4. Проіндексована для створення трикутників сітка вершин
Кожен квадрат ландшафту складається з двох трикутників: сірого і білого. У першому квадраті сітки сірий трикутник утворюють вершини 0, 1 і 7, а білий трикутник утворюють вершини 0, 7 і 6. Зауважте, що порядок індексів трикутника важливий: вони повинні слідувати за годинниковою стрілкою, оскільки конвеєр візуалізації XNA за замовчуванням відкидає трикутники у яких вершини йдуть проти годинникової стрілки.
Зверніть увагу на шаблон призначення індексів, використовуваних для створення трикутників, де індекси першого і другого трикутників кожного квадрата слідують одному і тому ж порядку, що показано в наступних формулах:
У показаних формулах значення змінної VertexCountX дорівнює кількості вершин у рядку сітки вершин. Використовуючи показання вище формули ви можете в циклі перебирати квадрати сітки вершин, генеруючи індекси їх трикутників. Ви генеруєте індекси сітки у вигляді масиву цілочисельних значень, в якому для кожного трикутника відведено три елементи. Ось код методу GenerateTerrainIndices:
Генерація розташування вершин і координат текстури
У цьому розділі ви створите метод GenerateTerrainVertices для генерації вершин сітки. Ви розміщуєте вершини ландшафту в світовій площині XZ, центруючи ландшафт по світовій позиції (0, 0). Для цього вам спочатку треба обчислити половину розміру ландшафту по осях X і Z, а потім встановити стартову позицію ландшафту на мінус половину його розміру по осях X і Z (-halfTerrainWidth. -halfTerrainDepth).
Ви можете обчислити розмір ландшафту через його атрибути: vertexCountX. який зберігає кількість вершин ландшафту по осі X; vertexCountZ. який зберігає кількість вершин ландшафту по осі Z; і blockScale. який зберігає відстань між сусідніми вершинами по осях X і Z. Після обчислення розмірів ландшафту вам треба просто розділити їх на два, як показано нижче:
Ви генеруєте грати вершин ландшафту, починаючи зі стартової позиції ландшафту і проходячи по кожному рядку решітки вершин, розміщує вершини (проходячи від -X до + X), переходячи потім до наступної лінії решітки (проходячи від -Z до + Z). Таким чином вершин решітки призначаються координати, збільшуються уздовж осей X і Z, відповідно до заданого вами розміром блоку, як показано на рис. 10.2. В ході розміщення вершин ви використовуєте раніше збережені дані карти висот, щоб встановити висоту вершини по осі Y. Ви також масштабіруете висоту ландшафту, множачи висоту кожної вершини на коефіцієнт масштабування: атрибут heightScale класу Terrain. Для коректного розміщення вершин в решітці ландшафту можна використовувати наступний код:
У кожної вершини також є координати текстури U і V, які повинні змінюватися від (0, 0) до (1, 1), де (0, 0) це початкова координата текстури, а (1, 1) - кінцева координата текстури. На рис. 10.5 показані координати текстури для деяких вершин в решітці.

Мал. 10.5. Координати текстури для вершин решітки (зліва). Осі UV на текстурі (праворуч)
Щоб обчислити правильні координати текстури для кожної вершини в ландшафті вам спочатку потрібно обчислити приріст координат текстури по осях UV. Для цього розділимо максимальне значення координати текстури (1.0) на кількість вершин по кожній з осей мінус одиниця:
Потім ви перебирає всі вершини, встановлюючи їх координати текстури і збільшуючи їх. Крім розташування і координат текстури вам треба обчислити нормаль, дотичну і бінормаль для кожної вершини. Для цього створимо методи GenerateTerrainNormals і GenerateTerrainTangentBinormal. які ви викличете в кінці методу GenerateTerrainVertices. Нижче показаний повний код методу GenerateTerrainVertices:
Генерація нормалей вершин
Вектор нормалі кожної вершини трикутника дорівнює вектору нормалі трикутника. Отже, щоб обчислити нормалі вершин трикутника вам треба обчислити нормаль трикутника. Ви обчислюєте нормаль трикутника, знаходячи векторний добуток між двома векторами, які формувались з його вершин, (v1 - v0) і (v2 - v0), оскільки векторне твір повертає вектор перпендикулярний цим двох векторах.
Оскільки в решітці вершин окрему вершину можуть спільно використовувати від одного до шести трикутників, нормаль кожної вершини є сумою нормалей тих трикутників, які спільно використовують цю вершину. Отже, вам необхідно обчислити вектори нормалі для кожного трикутника і підсумувати їх, щоб отримати нормаль вершини цих трикутників. В кінці ви повинні нормалізувати нормаль кожної вершини, щоб вона була одиничної довжини. Вектори нормалі використовуються в розрахунках освітлення, і щоб в результаті отримати правильне освітлення вони повинні бути одиничної довжини. Використовуйте наступний код для методу GenerateTerrainNormal. генеруючого нормалі вершин ландшафту:
Генерація дотичних і бінормаль вершин
Власний ефект, який ви створите для ландшафту, використовує техніку, звану накладення нормалей (normal mapping), що дозволяє збільшити деталізацію ландшафту без збільшення складності його сітки. Щоб використовувати техніку накладення нормалей, у кожної вершини сітки повинні бути вектори дотичній, бинормали і нормалі. Вектори дотичній, бинормали і нормалі взаємно перпендикулярні і утворюють дотичну (або тангенціальну) систему координат. На рис. 10.6 показані вектори дотичній, бинормали і нормалі для різних точок двох різних поверхонь.

Мал. 10.6. Вектори дотичній (T), бинормали (B) і нормалі (N)
Ви можете обчислити вектор дотичної кожної вершини в решітці вершин, як вектор, що починається в цій вершині і закінчується в наступній вершині решітки. Таким чином, вектори дотичній будуть паралельні осі X решітки. Зауважте, що вектор дотичної останньої вершини в рядку решітки обчислюється як вектор, що починається в передостанній вершині рядки і закінчується в останній вершині.
Після обчислення вектора дотичної ви можете отримати вектор бинормали, виконавши векторне множення між дотичній і нормаллю вершини. Мал. 10.7 показує вектори дотичних, бінормаль і нормалей плоскою решітки вершин.

Мал. 10.7. Вектори дотичних (T), бінормаль (B) і нормалей (N) деяких вершин плоскою решітки
Використовуйте наступний код методу GenerateTerrainTangentBinormal для обчислення векторів дотичній і бинормали вершин: