Вы находитесь на странице: 1из 77

Министерство образования Республики Беларусь

Учреждение образования
«Гомельский государственный университет
имени Франциска Скорины»

Факультет математический
Кафедра вычислительной математики и программирования

Допущена к защите
Зав. кафедрой______________ Лубочкин А.В.
(подпись)
«____»_________________ 2009 г.

РАЗРАБОТКА СИМУЛЯТОРА ДОРОЖНОГО ДВИЖЕНИЯ


С ИСПОЛЬЗОВАНИЕМ БИБЛИОТЕКИ DIRECTX
название темы дипломной работы

Дипломная работа

Исполнитель:
студент группы ПО-52 ________________ Демусев К. А.
шифр и номер группы подпись

Научный руководитель:
к.ф.-м.н., доцент ________________ Ружицкая Е. А.
подпись

Рецензент:
к.ф.-м.н., доцент ________________ Фамилия И. О.
подпись

Гомель 2009
2

СОДЕРЖАНИЕ
_

Введение...................................................................................................................3
П.Б Результаты работы программы.....................................................................77
3

ВВЕДЕНИЕ
При программировании графических приложений наибольшее внимание
должно быть уделено скорости обработки графики, а также современным
тенденциям в написании таких программных продуктов, для чего
используются унифицированные библиотеки обработки графики, одной из
которых является библиотека DirectX. Данная библиотека используется
преимущественно для написания видеоигр, использующих современные
возможности видеоадаптеров, что свидетельствует о рациональности
применения этой библиотеки при разработке графических приложений
любой сложности.
Написание сложных графических приложений при прямом обращении к
интерфейсам библиотеки DirectX является практически невозможным,
поэтому необходимо перед программированием таких приложений
разработать набор библиотек, предоставляющих основные функции по
обработке отдельных объектов трехмерной графики. Такой набор библиотек
называется ядром графического приложения (в англоязычной терминологии
используется название «graphic engine»). Существует большое количество
готовых библиотек, предоставляющих огромные возможности
программирования видеоигр и других мультимедийных приложений.
Целью данной работы является построение собственного графического ядра
без использования готовых работ третьих производителей. Графическое ядро
должно содержать большинство основных функций, необходимых при
написании видеоигр, и должно быть максимально оптимизировано в плане
производительности.
Среди основных функций должны быть следующие: создание и
инициализация графической составляющей приложения, обработка и
отображение двумерной графики, работа с виртуальной камерой, работа с
устройствами ввода, обработка и отображение трехмерных статических
объектов, обработка и отображение трехмерных анимированных объектов,
работа с графическими шейдерами, и quad-деревьями.
Технология шейдеров используется как при написании современных
видеоигр, так и при создании демонстрационных сцен, при моделировании
физических и химических процессов и при разработке сложных
программных комплексов, целью которых является отображение
графических объектов в наиболее реалистичном виде. Расчет освещения одна
из наиболее часто используемых возможностей шейдерных программ.
На данную работу были поставлены следующие задачи:
1 Изучить общие приемы работы с библиотекой DirectX в целом, ее
графическим компонентом Direct3D и функциями, ускоряющими реализацию
комбинированных математических алгоритмов.
2 Разработать основу графического ядра представляющую возможности
быстрой инициализации приложений, построенных на основе использования
трехмерной графики, выводу основных графических примитивов, загрузки и
отображения сложных трехмерных объектов из файла, позиционирования
4

виртуальной камеры в трехмерной сцене, и позволяющую осуществлять


взаимодействие пользователя с программой.
3 Встроить в ядро классы, позволяющие максимально сократить сроки и
затраты на разработку программных продуктов, касающихся скелетной
анимации, комбинированию анимаций, максимально полной обработке
анимации в реальном времени. Предоставить конечному пользователю
максимально полный набор функций, необходимый для наследования
разработанных классов для включения дополнительных возможностей с
минимальными затратами.
4 Добавить возможности, позволяющие разрабатывать программные
продукты, касающихся обработки объектов с использованием программ
вершинных и пиксельных шейдеров, комбинированию этих программ,
максимально полной обработке эффектов в реальном времени. Предоставить
конечному пользователю максимально полный набор функций, необходимый
для наследования разработанных классов и включения дополнительных
возможностей с минимальными затратами.
5 Изучить возможности оптимизации графических программ при
визуализации трехмерных сцен путем разделения пространства.
Использовать полученные в ходе исследования навыки при разработке
классов, позволяющих разделять пространство и быстро ориентироваться в
его составляющих.
6 Разработать программный продукт, видеоигру «Симулятор дорожного
движения», который построен на основе разработанного ядра и реализует
большинство его возможностей. Видеоигра предназначена как для
демонстрации разработанных в библиотеках ядра функций, так и для
использования пользователями в развлекательных целях.
7 В ходе разработки использовать подход, позволяющий использовать
компоненты разработанных библиотек и ядра в других приложениях,
обеспечить максимально возможную масштабируемость и аппаратную
независимость.
Решение задания осуществляется при помощи обзора основных методов
работы с библиотекой DirectX, изучения взаимодействия графического
компонента данной библиотеки с оборудованием, пошаговой разработки
ядра графического приложения, реализующего современные функции
обработки трехмерной графики и оптимальные пути решения проблем
оптимизации скорости ее обработки, а также разработки приложения-
видеоигры, построенного на основе разработанного ядра, предназначенного
как для демонстрации профессиональных приемов построения сложных
трехмерных графических приложений, а также алгоритмы, основанные на
применении аналитической геометрии и линейной алгебры. При решении
задания также необходимо организовать диалог разработанной программы с
пользователем для интерактивной демонстрации всех возможностей
графического ядра и алгоритмов обработки трехмерной графики и
построения видеоигр.
5

1 КОНЦЕПЦИЯ РАЗРАБОТКИ ГРАФИЧЕСКИХ ПРИЛОЖЕНИЙ


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

1.1 Характеристика COM-объектов

Библиотека DirectX состоит из нескольких СОМ-объектов. Технология СОМ


(Component Object Model) является дальнейшим развитием объектно-
ориентированного программирования и направлена на создание компонентов
программного обеспечения.
СОМ-объекты и их интерфейсы идентифицируются числами, занимающими 16
байт, которые называются GUID – глобально уникальные идентификаторы.
Для обеспечения изоляции СОМ-сервер не экспортирует имена объявленных в
нем переменных, а СОМ-объект не содержит полей данных типа public. Он
взаимодействует с приложением только при помощи методов, сгруппированных в
интерфейсы. Интерфейс – это набор методов, унаследованных объектом от
одного абстрактного класса, содержащего только заголовки виртуальных
функций.
Объект может предоставлять приложению несколько интерфейсов, В C++ они
наследуются от нескольких предков на основе механизма множественного
наследования. Имена классов, которыми СОМ-объекты реализуются в сервере,
не помещаются в заголовочные файлы, доступные прикладной программе.
Поэтому, если в сервере реализован некоторый класс, в прикладной программе
нельзя создать экземпляр этого класса, а затем обратиться к его методу.
Это позволяет исключить возможность случайного совпадения имен разных
объектов и обеспечить их коммерческое тиражирование. Но этот же механизм
лишает приложения возможности пользоваться стандартными способами
создания объектов и вызова их методов. Эта проблема разрешается
существующими в операционной системе Windows средствами поддержки
СОМ-технологии и требованиями к реализации СОМ-сервера.
Компонент Direct3D библиотеки DirectX является ничем иным, как COM-
объектом.
6

1.2 Графический конвейер и кадрирование изображения

Все трехмерные объекты состоят из треугольников – простейших объектов


графической библиотеки, - которые характеризуются, в первую очередь,
вершинами. Вершины – это точки, имеющие 3 координаты в пространстве по
осям X, Y, Z.
Рассмотрим процесс получения изображения этих треугольников. После
расположения треугольников в трехмерном пространстве в соответствии с
координатами их вершин, строится пирамида видимости (вид), которая
представляет собой виртуальное геометрическое тело – усеченную пирамиду,
меньшим основанием которой является проекционная плоскость (рисунок
1.1). Те объекты которые попадают в эту пирамиду, будут отрисовываться.

Рисунок 1.1 – Пирамида видимости

Экранные координаты вершин объектов, попадающих в пирамиду видимости


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

Рисунок 1.2 – Канонический куб


7

Трехмерные мировые координаты и координаты проекций задают


вещественными числами. После приведения пирамиды видимости к
каноническому объему все проекции попадают в квадрат размером 1x1 или 2
х 2, а при отображении квадрат растягивается до размеров рисунка. Кроме
того, при выводе данных на графическое устройство необходимо задавать
целочисленные координаты адресуемых позиций плоского носителя
изображения. Операция масштабирования канонического квадрата и
преобразования вещественных координат проекционной плоскости в
целочисленные координаты носителя называется кадрированием
изображения.
Для формирования изображения в графической системе может быть
выделено несколько участков памяти — буферов. Кадровый буфер,
хранящий цвет пикселов, называют также буфером цвета. Для удаления
невидимых точек создается буфер, хранящий расстояние от наблюдателя
каждой точки, записанной в буфер цвета. Он называется Z-буфером или
буфером глубины.
Для повышения реалистичности при отображении граней используются
различные методы их закраски и текстурирования. Таким образом, модель в
процессе вывода на отображающее устройство подвергается
последовательности преобразований, называемой видовым или графическим
конвейером (а также просто конвейером).
Библиотеки OpenGL и DirectX обеспечивают реализацию всех этапов
графического конвейера.

1.3 Вывод треугольников

Порядок вывода примитивов после получения интерфейса IDirect3DDevice9


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

обрабатываемый элемент массива вершин, количество обрабатываемых вершин,


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

1.4 Материалы и текстурные карты

Для построения графических примитивов зачастую мало только указать


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

Рисунок 1.3 – Наложение текстурной карты на примитив


9

Помимо применения цвета и свойств реакции на свет, к вершине можно


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

1.5 Методы освещенности

При вычислении цвета вершин с учетом условий освещенности и материала


освещенной поверхности принято искусственно разделять рассеянный,
диффузный и зеркальный свет.
При падении света на поверхность часть его отражается, а часть поглощается.
Коэффициенты отражения также назначаются раздельно для каждого типа
света и каждого цветового компонента и называются рассеянным,
диффузным и зеркальным цветом материала. Это позволяет имитировать
освещение в типичных ситуациях.
Рассеянный (ambient) свет моделирует результат многократного отражения
лучей источника света от большого числа поверхностей. При этом можно
считать, что свет падает на объект со всех сторон. При этом объект освещен, но
не отбрасывает тени, и цвет грани не зависит от ее ориентации в пространстве.
Если шар из однородного материала аппроксимируется многогранником, то все
его грани имеют один цвет и шар выглядит как круг.
Диффузный свет моделирует отражение света матовой поверхностью. При этом
интенсивность отраженного света пропорциональна косинусу угла между
нормалью к поверхности и направлением на источник света. Таким образом,
видимый цвет грани зависит от ее ориентации в пространстве. Если свет падает
перпендикулярно к поверхности, то яркость будет максимальной. При
диффузном отражении весь свет как бы поглощается поверхностью, а потом
излучается ею во всех направлениях. Поэтому видимый цвет грани не зависит
от перемещения точки наблюдения.
Зеркальный свет и зеркальный цвет материала имитируют в совокупности
отражение света блестящей поверхностью. При этом угол падения света равен
углу отражения, поэтому при неизменном положении источника света и грани
ее цвет изменяется при перемещении наблюдателя. Грань выглядит ярко
освещенной, если отраженный от нее луч проходит через точку наблюдения.
При зеркальном отражении цвет отраженного луча совпадает с цветом
падающего и не должен зависеть от цвета поверхности. Так, при освещении
красного елочного шара зеленым лучом наблюдатель увидит на его поверхности
зеленый блик. Но обеспечение этого свойства в библиотеке не предусмотрено.
Цвет отраженного луча отличается от цвета падающего, так как в материале
задаются индивидуальные коэффициенты для каждого компонента цвета.
Для вычисления цвета вершины необходимо задавать для каждого типа
освещения цвет источника, цвет материала и нормаль к поверхности.
Графическая библиотека для имитации условий освещенности реальных объектов
позволяет хранить и обрабатывать в графическом конвейере характеристики
10

материала и нескольких источников света. При вычислении цвета точки


суммируются результаты, создаваемые всеми типами освещения и всеми
источниками света.
При включенном расчете освещенности детали его применения зависят от
типа вершин. Вершины, содержащие экранные координаты используют
цвет вершин без преобразования. При отсутствии в трехмерной вершине
нормалей ее цвет получается черным. Если нормали вершин треугольника
равны (0.0, 0.0, 1.0), то при освещении созданным выше источником
треугольник также получается черным. Если же нормали равны (0.0, 0.0,
-1.0), то треугольник имеет максимальную яркость. Созданный источник
светит вдоль положительного направления оси Z, поэтому при значении
(0.0, 0.0, -1.0) грань ориентирована перпендикулярно падающему свету.
Нормаль равна (0.0, 0.0, 1.0) для той стороны грани, которая отвернута от
источника.
Шейдеры решают одну из самых распространенных проблем, возникающих
при программировании графики – проблему освещения. При создании
освещения стандартными средствами библиотеки DirectX – возникает эффект
неравномерности освещения. Шейдер позволяет создать освещение,
максимально приближенное к реалистичности. Сравнение типов освещений
приведено на рисунке 1.4 (слева – освещение, созданное при помощи
шейдеров).

Рисунок 1.4 – Сравнение типов освещения

При разработке освещения стандартными средствами библиотеки DirectX


освещение получается «резаным» из-за того, что каждая грань освещается
одним тоном полностью, соответственно грани, попавшие в линию границы
освещения визуализируются освещенными, а грани, лежащие за линией –
неосвещенными. Шейдеры решают данную проблему, так как позволяют
каждый пиксель грани обрабатывать независимо от других.
Модель диффузного освещения дает очень неплохой результат, так как
интенсивность освещения каждой точки модели рассчитывается в
зависимости от направления нормали поверхности и позиции источника
света. В результате получается реалистичное затенение объекта.
11

2 РАЗРАБОТКА БИБЛИОТЕКИ-ЯДРА
При написании сложных мультимедийных приложений недостаточно
использования одной только библиотеки, предоставляющей
унифицированный способ взаимодействия с графическим конвейером.
Необходимо разработать библиотеку, являющуюся ядром разрабатываемого
приложения (в англоязычном варианте ядро графического приложения
называется Engine). Данная библиотека предоставит следующие
возможности:
– создание и инициализация графической составляющей приложения;
– обработка и отображение двумерной графики;
– работа с виртуальной камерой;
– работа с устройствами ввода (клавиатурой и мышью);
– обработка и отображение трехмерных статических объектов;
– обработка и отображение трехмерных анимированных объектов;
– работа со счетчиком времени для использования при анимации;
– отображение текстовой информации;
– работа с графическими шейдерами;
– работа с quad-деревьями;
– работа с ландшафтами;
– вспомогательные математические функции.
При разработке такой библиотеки ей было присвоено название Eleky,
исходный код которой представлен в приложении A. В библиотеке
присутствуют следующие классы:
– CEleky, предназначенный для инициализации приложения;
– CElekyCamera, выполняющий функции по работе со свободной камерой;
– CEleky3rdPersCamera, обеспечивающий работу камеры с видом от третьего
лица
– СElekySprite для работы с двухмерной графикой;
– CElekyText разработанный для отображения текстовой информации;
– CElekyTerrain выполняющий работу с ландшафтами, основанными на карте
высот;
– CElekyMesh для работы с объектами, хранящимися в файлах .x;
– СElekyTiming обеспечивающий синхронизацию по времени;
– CElekyCulling оптимизирующий обработку и вывод графики;
– и некоторые другие вспомогательные классы.
Для унификации интерфейса элементов разработанной библиотеки каждый
из классов при необходимости имеет функцию init, необходимую для
загрузки и инициализации данных объекта, возвращающую переменную типа
bool, принимающую значение true в случае успешной инициализации и false
в случае неудачи, а также функцию update, предназначенную для обновления
сведений об объекте, и функцию render, вызываемую при необходимости
отрисовки объекта в графический конвейер. Конструктор каждого из классов
принимает как минимум указатель на объект IDirect3dDevice9,
12

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


большинства функций библиотеки Direct3D.

2.1 Инициализация графической составляющей приложения


После создания главного окна программы требуется инициализация библиотеки DirectX. В
DirectX реализованы как оконный так и полноэкранный уровни привилегий при работе с
экраном. В полноэкранном режиме предполагается, что весь экран как ресурс предоставляется
одному приложению. Поэтому приложению разрешено программно изменять видеорежим,
причем как размер растра, так и формат пиксела. Возможные форматы пиксела, которые в
терминах DirectX называются форматом поверхности, определяют распределение битов
ячейки буфера цвета между компонентами цвета и коэффициентом прозрачности.
В полноэкранном режиме DirectX позволяет организовать тройную
буферизацию, позволяющую избежать потерь времени на ожидание
обратного хода луча по кадру при аппаратном переключении страниц. А
при двойной буферизации в этом режиме можно отменить ожидание
перед началом копирования обратного хода луча по кадру.
Настройка графической системы должна происходить перед созданием
экземпляра устройства. Настроечные данные в Direct3D сведены в структуру
_D3DPRESENT_PARAMETERS_.
Методы объекта Direct3D9 позволяют определить количество графических
адаптеров, текущее состояние аппаратуры, а также возможности
конвейера при программной или аппаратной реализации. Функции
графических ускорителей не стандартизованы. При помощи метода
объекта Direct3D9 можно организовать проверку возможностей
аппаратуры.
Работа с библиотекой начинается с создания объекта Direct3D9 и
получения указателя на его интерфейс IDirect3D9 функцией Direct3DCreate9.
После этого при помощи метода CreateDevice интерфейса IDirect3D9
создается объект Direct3DDevice9. Его интерфейс IDirect3DDevice9 содержит
множество методов, обеспечивающих формирование изображения. Они
позволяют обеспечить вывод примитивов, определение размера текстурной
памяти и создание текстур, задание уровня доступа к экрану. Инициализация
библиотеки, собственно, и выполняется вызовом метода CreateDevice.
Этому методу передается структура D3DPRESENT_PARAMETERS,
задающая формат пиксела, размер и количество буферов, уровень доступа к
экрану и иные важные параметры.
При создании Direct3DDevice9 можно выбрать один из стандартных
вариантов реализации конвейера. Если при создании объекта
Direct3DDevice9 его тип указан константой D3DDEVTYPE_REF, то
библиотека не использует аппаратной поддержки ускорителя, но в
зависимости от типа процессора применяет команды 3DNow! и ММХ при
растровой развертке примитивов. Если же тип указан константой
D3DDEVTYPE_HAL, то библиотека использует аппаратную поддержку тех
функций, которые может выполнять графический ускоритель. Еще один
параметр при выборе D3DDEVTYPE_HAL позволяет оставить основному
13

процессору геометрические преобразования и расчет освещения, а


растровые операции передать ускорителю.

2.2 Обработка и отображение двумерной графики

Начиная с версии 8 в библиотеке Direct3D появился интерфейс ID3DXSprite,


предоставляющий возможности обрабатывать двумерные изображения и
выводить их в графический конвейер. Двумерные изображения в трехмерном
графике часто называют спрайтами. Спрайты используются повсеместно,
даже в полностью трехмерном приложении. Они необходимы как минимум
для отображения пользовательского интерфейса и визуализации текстовых
сообщений.
В разработанное ядро был включен компонент CElekySprite для работы с
двумерной графикой и упрощения ее обработки. Функция init разработанной
библиотеки принимает два параметра: наименование графического файла, из
которого будет загружен спрайт и цвет, который будет невидимым при
отображении данного спрайта на экране. Данная функция вызывает функции
D3DXCreateSprite и D3DXCreateTextureFromFileEx библиотеки Direct3D для
создания спрайта и загрузки в него изображения.
На самом деле библиотека Direct3D требует создания объекта спрайта один
лишь раз и затем в вызывается метод Draw для рисования к графическом
конвейере текстуры, загруженной любым доступным для Direct3D образом.
Код рисования спрайта представляет фунция render (листинг 2.1), которая
принимает в качестве параметров координаты x и y области экрана (начиная
с верхнего левого угла, в отличие от трехмерной сцены) и струткуру типа
RECT для определения исходного прямоугольника в загруженной текстуре.

Листинг 2.1 – Рисование двумерного объекта


void CElekySprite::render(float x, float y, RECT *pSrcRect)
{
pSprite->Begin();
pSprite->Draw(pTexture, pSrcRect, NULL, NULL, 0, &D3DXVECTOR2(x, y),
0xffffffff);
pSprite->End();
}

Функция Draw интерфейса ID3DXSprite имеет более богатые возможности


для рисования двумерных объектов, например, третий параметр позволяет
растягивать или сжимать изображение, третий и четвертый – вращать
относительно установленного центра вращения. Таким образом, возможным
расширением компонента двумерных изображений разработанного ядра
является перегрузка метода render для реализации всех возможностей
рисования.
Для отображения текстовой информации и создания шрифта в библиотеке
Direct3D присутствует интерфейс ID3DXFont, однако, как показывает
практика, его использование не оправдано, поскольку создание шрифта и его
отображение используется через вызов подсистемы GDI операционной
14

системы Microsoft Windows, которая в свою очередь не проявляет большого


быстродействия.
Для решения данной проблемы в разработанное ядро был включен
компонент CElekyText, который напрямую зависит от объекта CElekySprite,
рассмотренном выше. Данный компонент наряду с другими членами
содержит указатель на объект типа CElekySprite, который представляет
загруженное изображение со шрифтом (рисунок 2.2). Основная идея данного
метода вывода текстовой информации состоит в том, чтобы отображать
символ из загруженного двумерного объекта, который находится в
определенной в конструкторе CElekyText позиции на экране. Метод init()
лишь создает новый объект типа CElekySprite и вызывает его метод init,
фактически загружает текстуру со шрифтом. Основную работу по выводу
шрифта и текста в графический конвейер выполняет функция render (листинг
2.2), которая принимает четыре параметра: собственно указатель на строку,
которую необходимо вывести, координаты x и y экрана (начиная с левого
верхнего угла) и максимальную ширину в пикселах строки.
На рисунке 2.1 представлен результат вывода текста в окно тестовой
программы. Сверху текст выводится во всю длину окна, а ниже он ограничен
максимальной шириной, в результате чего символы, которые не помещаются
в данную ширину, переносятся на следующую строку.

Рисунок 2.1 – Пример вывода тестовой информации

Листинг 2.2 - Вывод текста


void CElekyText::render(char *strText, LONG x, LONG y, int iWidth)
{
int iHeight = (int)ceilf((float)(strlen(strText) * iLetterWidth) /
(float)iWidth);
iHeight *= iLetterHeight;
for(size_t i = 0; i < strlen(strText); i++)
{
RECT dest;
dest.left = x;
dest.top = y;
dest.right = x + iLetterWidth;
dest.bottom = y + iLetterHeight;

RECT src;
src.left = fontBuffer[strText[i]] * iLetterWidth;
src.top = 0;
src.right = (fontBuffer[strText[i]]+1) * iLetterWidth;
src.bottom = iLetterHeight;
pFont->render(&dest, &src);
x += iLetterWidth;
if(x >= iWidth)
15

{
x = 0;
y += iLetterHeight;
}
}
}

2.3 Работа с ландшафтом

При программировании таких мультимедийных приложений как видеоигры


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

Рисунок 2.2 – Пример карты высот

Карта высот для разработанного ядра представляет восьмибитный


монохромный рисунок размером 200 на 200 пикселей, сохраненный в
формате raw без заголовка. В данном рисунке более темные точки
определяют наименьшую высоту, а более светлые – наибольшие. Значения
высоты могут находиться в диапазоне от 0 до 255. Хотя диапазон от 0 до 255
достаточен для хранения перепадов высот ландшафта, в приложении может
потребоваться масштабирование этого значения, чтобы оно соответствовало
масштабу трехмерного мира.
Карта высот может быть создана либо программно, либо с помощью
графического редактора, например Adobe Photoshop. Использование
графического редактора — более простой способ, позволяющий
интерактивно создавать ландшафты и видеть полученный результат. Кроме
того, можно создавать необычные карты высот, пользуясь дополнительными
возможностями графического редактора, такими как фильтры.
На рисунке 2.3 представлен вид ландшафта, сгенерированного из карты
высот, представленной на рисунке 2.2.
16

Рисунок 2.3 – Ландшафт, сгенерированный из карты высот

Каждая точка карты высот соответствует одной вершине. В библиотеке


DirectX для описания вершины служит специальная структура D3DVERTEX.
Она содержит помимо координат вершины, нормаль к поверхности,
позволяющую вычислить цвет точки с учетом освещения и заданного
материала. При этом приложению остается только поочередно выбирать
из модели мира вершины составляющих объект граней и передавать их в
конвейер.
Программист сам определяет состав полей для описания точки, что
позволяет достигать максимальной производительности графической
системы, учитывая специфику задачи и выравнивания загрузки процессора
и акселератора. Поля описания вершины могут содержать координаты
точки на экранной плоскости и в пространстве, один или два кода цвета,
характеризующих материал поверхности, вектор нормали и несколько пар
текстурных координат. Информация о выбранной структуре заносится в
конвейер при помощи метода SetFVF интерфейса IDirect3DDevice9,
которому передается сочетание флагов.
Для задания двумерного треугольника потребуется объявить массив из трех
элементов типа рассмотренной структуры. При этом графическая библиотека
во время вывода примитивов кроме растровой развертки будет выполнять
только отсечение на плоскости по границам окна. Конвейер библиотеки не
должен выполнять проецирование и вычисление освещенности, так как
это уже было вычислено приложением. Таким образом, независимо от
того, есть ускоритель в системе или нет, указанные операции удалены из
конвейера, потому что их выполняет приложение.
17

Если нужно задействовать все этапы конвейера, то в структуру надо


включить нормали, текстурные координаты и координаты вершины в
пространстве.
После задания координат и других свойств вершин треугольника требуется
создать буфер вершин и занести в него созданный экземпляр структуры. Для
отображения полученного примитива вызывается один из методов рисования
графических примитивов Direct3D, например DrawPrimitive интерфейса
устройства. И, наконец, требуется выполнить метод Present интерфейса
устройства для переключения страниц.
Для создания вершин ландшафта в ядре программы используется буфер
индексов. Буфер индексов помогает оптимизировать выполнение программ, в
частности вывод графических примитивов на экран, путем уменьшения
количества копируемых вершин в буфер визуализации.
Для отображения сетки ландшафта используется функция
DrawIndexedPrimitive в методе render.
После конструирования ландшафта, необходимо добавить возможность
определения высоты ландшафта (координату Y) в любой заданной
координате X и Z. Это позволит позиционировать объекты на ландшафт в
соответствии с правильной высотой. Для этого сначала необходимо
определить по координатам X и Z камеры квадрат ландшафта в котором
находится искомая точка. Все это делает функция CElekyTerrain::getHeight в
своих параметрах она получает координаты X и Z камеры и возвращает
высоту, на которой должен быть расположен объект, чтобы он оказалась над
ландшафтом (листинг 2.3)

Листинг 2.3 – Определение высоты ландшафта


float CElekyTerrain::getHeight(float x, float z)
{
// Выполняем преобразование перемещения для плоскости XZ,
// чтобы точка START ландшафта совпала с началом координат.
x = ((float)iWidth / 2.0f) + x;
z = ((float)iDepth / 2.0f) - z;
// Масштабируем сетку таким образом, чтобы размер
// каждой ее ячейки стал равен 1.
x /= (float)iSpacing;
z /= (float)iSpacing;
float col = floorf(x);
float row = floorf(z);
// A B
// *---*
// | / |
// *---*
// C D
float A = (float)getHeightmapForLerp((int)row, (int)col);
float B = (float)getHeightmapForLerp((int)row, (int)col+1);
float C = (float)getHeightmapForLerp((int)row+1, (int)col);
float D = (float)getHeightmapForLerp((int)row+1, (int)col+1);
float dx = x - col;
float dz = z - row;
float height;
if(dz < 1.0f - dx) // верхний треугольник ABC
{
float uy = B - A; // A->B
18

float vy = C - A; // A->C
height = A + Lerp(0.0f, uy, dx) + Lerp(0.0f, vy, dz);
}
else // нижний треугольник DCB
{
float uy = C - D; // D->C
float vy = B - D; // D->B
height = D + Lerp(0.0f, uy, 1.0f - dx) + Lerp(0.0f, vy, 1.0f -
dz);
}
return height;
}
inline float CElekyTerrain::Lerp(float a, float b, float t)
return a - (a*t) + (b*t);

Функция Lerp выполняет линейную интерполяцию вдоль одномерной линии,


getHeightmapForLerp возвращает высоту ландшафта в карте высот в
соответствии с переданными координатами.
Вначале функция getHeight выполняет перемещение в результате которого
начальная точка ландшафта будет совпадать с началом координат. Затем
выполняется операцию масштабирования с коэффициентом равным единице
деленной на размер клетки; в результате размер клетки ландшафта будет
равен 1. Далее происходит переход к новой системе координат, где
положительное направление оси Z направлено «вниз». Измененная
координатная система соответствует порядку элементов матрицы. То есть
верхний левый угол — это начало координат, номера столбцов
увеличиваются вправо, а номера строк растут вниз. Следовательно, помня о
том, что размер ячейки равен 1, видно что номер строки и столбца для той
клетки на которой мы находимся вычисляется так: float col = floorf(x); float
row = floorf(z);
После получения информации и ячейке нахождения можно получить высоты
ее четырех вершин. Из полученных данных необходимо найти высоту
(координату Y) точки ячейки с указанными координатами X и Z. Чтобы
определить высоту необходимо узнать в каком треугольнике ячейки
требуется вычислить высоту. Каждая ячейка визуализируется как два
треугольника. Чтобы определить в каком треугольнике находится искомая
координата, необходимо выбрать найденный квадрат сетки и переместить его
таким образом, чтобы верхняя левая вершина совпадала с началом
координат.
Поскольку переменные row и col определяют местоположение левой верхней
вершины той ячейки, необходимо выполнить перемещение на –col по оси X и
–row по оси Z: float dx = x - col; float dz = z – row.
На рисунке 2.4 представлена ячейка до и после преобразования,
переносящего ее верхнюю левую вершину в начало координат
Теперь, если dz < 1.0 – dx то нахождение координаты в «верхнем»
треугольнике Δv0v1v2. В ином случае – в «нижнем» треугольнике Δv0v2v3.
Чтобы найти высоту в «верхнем» треугольнике, необходимо сформировать
два вектора u = (cellSpacing, B – A, 0) и v = (0, C – A, –cellSpacing),
совпадающих со сторонами треугольника и начинающихся в точке, заданной
вектором q = (qx, A, qz). Затем выполняется линейную интерполяцию вдоль
19

вектора u на dx и вдоль вектора v на dz. Координата Y вектора (q + dxu + dzv)


дает высоту для заданных координат X и Z.

Рисунок 2.4 – Ячейка до и после преобразования

Поскольку необходимо вычислить только значение высоты, можно


выполнять интерполяцию только для компоненты y и игнорировать
остальные компоненты. В этом случае высота определяется по формуле
A + dxuy + dzvy.

2.4 Работа со статическими объектами

При программировании графических приложений часто возникает задача


рисования не только треугольников, но и более сложных полигональных
примитивов. Однако, как было сказано выше, Direct3D поддерживает
рисование только точек, линий и треугольников, из которых и состоят более
сложные полигональные примитивы. Действительно, любой объект, даже
самой сложной формы, можно построить при помощи списка треугольников,
расположенных определенным образом и имеющим определенные размеры.
Практически невозможно создать объект сложной формы путем указания
координат всех треугольников, входящих в объект. Поэтому для
моделирования сложных объектов созданы пакеты трехмерной графики,
которые позволяют экспортировать визуально созданные объекты в
различные форматы трехмерных моделей. Существует возможность
загружать один из таких форматов в программы, написанные с
использованием библиотеки DirectX.
Разработанное в ходе выполнения данной курсовой работы графическое
приложение работает с такими объектами, а также позволяет отобразить не
сглаженную поверхность, а линии, соединяющие вершины треугольников, из
которых и состоят все объекты. Таким образом вывод полигональных
объектов подразумевает, прежде всего, отрисовку треугольников,
позицированных специальным образом в пространстве.
20

Для загрузки объекта из файла служит функция D3DXLoadMeshFromX,


которая принимает одним из своих параметров указатель на интерфейс
ID3DXMesh, который позволяет работать далее с полигональным объектом.
Также рассмотренная функция принимает одним из своих параметров
указатель на буфер материалов.
После вызова указанной функции разработчик должен скопировать из
буфера материалов все материалы в структуру D3DXMATERIAL9 и
загрузить требуемые текстуры, имена которых также находятся в буфере
материалов.
Каждый объект состоит из частей, на которые он делится согласно
примененным материалам. Отображение полигонального объекта, после
применения к нему матрицы трансформации, производится с помощью
метода DrawSubset интерфейса ID3DXMesh, принимающего параметром
номер части. Для вывода всех частей объекта требуется определить
количество материалов объекта и использовать рассмотренный выше метод,
передавая ему значения от нуля до определенного количества материалов.
В ходе разработки ядра-библиотеки был создан класс CElekyMesh,
реализующий такие возможности по работе со статическими
полигональными объектами как: загрузка объекта из файла, абсолютное его
позиционирование и вращение и отрисовку объекта. Также в классе
присутствует возможность загрузки объекта не из файла, а из другого
объекта, что позволяет экономить память.
Следующий пример иллюстрирует данную экономию: пусть необходимо на
ландшафте отобразить 1000 деревьев, размещенных случайным образом,
которые представлены объектами в формате .x в четырех вариантах. Так
прямое решение предполагает загрузку всех 1000 объектов в память их
перемещение и отрисовку. Данное решение заняло бы огромное количество
памяти компьютера, в зависимости от количества полигонов для каждого
объекта. Решение, реализованное в разработанном ядре позволяет
сэкономить память в более чем десятки раз, путем загрузки всего четырех
разных объектов из файла .x и создания всего четырех объектов типа
ID3DXMesh. Остальные 996 объектов CElekyMesh хранят только указатель
на необходимый ID3DXMesh из первых четырех, преобразует его и выводит
на экран в другой позиции. Данный подход не только экономит огромное
количество памяти, но и позволяет существенно увеличить скорость
визуализации, что установлено эмпирическим путем. На рисунке 2.5
представлен ландшафт с случайным образом расположенными деревьями,
загруженными из 4 разных объектов.
21

Рисунок 2.5 – Ландшафт с большим количеством повторяющихся объектов

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

Одним из самых больших компонентов разработанного ядра является


компонент работы со скелетной анимацией. Данный вид анимации является
широко распространенным и используется при анимировании людей,
животных и других объектов. Скелетная анимация представляет
преимущество при программировании анимаций как в плане экономии
памяти, так и в плане скорости разработки анимации. Для скелетной
анимации не нужно хранить весь набор вершин для каждого ключевого
кадра, достаточно хранить вершины так называемых «костей».
Для работы со скелетной анимацией необходимо ее правильно загрузить. В
начале необходимо загрузить каркас, данные о применении вершин к костям,
а затем отдельно загрузить иерархию костей. Хранить иерархию костей
отдельно от каркаса – это интересное решение, потому что позволяет
применять различные иерархии к одному каркасу.
Для хранения иерархии фреймов была разработана структура
D3DXFRAME_EX, которая является наследницей структуры D3DXFRAME,
предоставляемой интерфейсом DirectX. Структура D3DXFRAME (или
унаследованная D3DXFRAME_EX) содержит два указателя, предназначенных
для создания иерархии – pFrameSibling (для родителей) и pFrameFirstChild (для
дочерних узлов). Основная задача на данном этапе разработки - связать каждый
загружаемый из .x файла фрейм, используя эти два указателя.
Для каждого найденного объекта Frame в .x файле создается соответствующий
объект и привязывается к иерархии объектов.
Для обработки .x файлов, был разработан класс cXFrameParser, который
выполняет анализ .x файла. В данном классе была создана функция
ParseObject, которая предоставляет доступ к данным всех объектов.
22

Обычно функция ParseObject вызывается для каждого перечисляемого


объекта. Внутри функции ParseObject проверяется тип текущего
перечисляемого объекта (используя его шаблонный GUID). Если тип
является Frame, создается структура фрейма и загружается его имя в нее.
Далее привязывается фрейм в иерархию фреймов. Класс cXFrameParser
содержит два указателя - один для созданного корневого фрейм-объекта
(m_RootFrame, член класса) и один для объекта данных (Data), который
передается в качестве параметра функции ParseObject при каждом ее вызове.
Указатель данных содержит данные последнего загруженного фрейм-объекта.
В начале анализирования .x файла, указатель Data установлен в NULL,
означая, что не было загружено никаких фрейм-объектов. Когда загружается
фрейм-объект, проверяется указатель данных, чтобы определить указывает
ли он на фрейм. Если нет, то предполагается, что текущий фрейм является
родственником корневого фрейма. Если же указатель данных указывает на
фрейм, то полагается что текущий перечисляемый фрейм является потомком
указываемого указателем данных фрейма.
Знание того, является ли текущий фрейм родственным или дочерним,
считается определяющим при создании иерархии. Родственные фреймы
связываются используя указатель pFrameSibling структуры D3DXFRAME, в
то время как дочерние фреймы связываются, используя pFrameFirstChild.
Сразу же после загрузки фрейма указатель данных устанавливается в новый
фрейм или в предыдущий родственный фрейм. В конце концов, все фреймы
связываются между собой как родственники или потомки.
Функция ParseObject содержит код для загрузки матрицы преобразования
(трансформации) фрейма (представленной шаблоном FrameTransformMatrix).
Обычно объект FrameTransformMatrix встраивается в объект Frame. Объект
FrameTransformMatrix определяет начальное положение загруженного фрейма.
Примененная к скелетной анимации, эта матрица преобразования фрейма
определяет начальную позу скелетной структуры. Например, стандартная
скелетная структура может находится в позе, в которой тело расположено
вертикально, а руки расставлены. Однако предположим, что все анимации
рассчитаны на персонаж, находящийся в другой позе, возможно с руками по
швам и чуть согнутыми ногами. Вместо того чтобы менять положение всех
вершин или костей, чтобы они соответствовали позе, перед сохранением .x
файла в программе трехмерного моделирования, можно изменить
преобразования фреймов. Таким образом, все движения костей будут
происходить относительно этой позы.
После загрузки иерархии костей, возможно управлять ими. Для изменения
положения кости необходимо обнаружить соответствующую ей структуру
фрейма. Разработана функция, которая рекурсивно просматривает фрейм на
совпадение с именем кости. Как только он будет найден, будет получен
указатель на него, который можно использовать для получения доступа к
матрице преобразования.
Возможно применять любые преобразования к кости в иерархии, но
рекомендуется использовать только повороты. Это объясняется тем, что в
23

большинстве случаев невозможно отделить часть объекта от целого (что дает


перемещение).
Если требуется переместить весь каркас, необходимо переместить корневую
кость; это преобразование распространится на все остальные кости. Также
можно использовать преобразование мира, чтобы двигать весь каркас-объект.
Для перемещения определенной кости требуется ее структура
D3DXFRAME_EX. Для поиска данной структуры была разработана функция
FindFrame, которой достаточно указать имя искомой кости и указатель на
корневой объект фрейм. Функция, после выполнения своей работы вернет
указатель на структуру D3DXFRAME_EX.
После изменения матрицы преобразования костей, необходимо обновить всю
иерархию, чтобы было возможно использовать ее для последующей
визуализации. Даже если матрица преобразования костей не была изменена,
все равно необходимо обновить иерархию для установления некоторых
переменных перед визуализацией.
Во время обновления иерархии необходимо комбинировать удачные
трансформации вниз по иерархии. Начиная с корня, накладывается матрица
преобразования костей на комбинированную матрицу преобразования
фреймов. Матрица преобразования костей накладывается также на все
родственные фреймы корневого. После этого только что посчитанная
матрица преобразования накладывается на все дочерние фреймы корневого.
Этот процесс распространяется на всю иерархию.
Самым простым способом обновления иерархии фреймов является создание
рекурсивной функции, которая бы комбинировала матрицу преобразования
фрейма с заданной матрицей преобразования. Далее матрица преобразования
передается родственникам, а комбинированная матрица - потомкам.
Разработана функция UpdateHierarchy, которая использует в качестве первого
параметра объект D3DXFRAME_EX, который является текущим
обрабатываемым фреймом. Необходимо вызвать функцию UpdateHierarchy
только один раз, задав указатель на корневой фрейм; функция рекурсивно
будет вызывать сама себя для каждого фрейма.
Второй параметр UpdateHierarchy - matTransformation. Параметр
matTransformation является матрицей преобразования, накладываемой на
преобразование фрейма. По умолчанию указатель matTransformation является
NULL, означая, что будет использоваться единичная матрица при вызове
UpdateHierarchy. После того как матрица фрейма комбинируется с заданным
преобразованием, результирующее преобразование передается дочерним
фреймам в качестве параметра matTransformation при следующих вызовах
функции.
Загрузка скелетных каркасов из .x файлов очень похожа на загрузку обычных
каркасов. Используя разработанный в рамках данной работы анализатор .x
файлов, необходимо перечислить объекты, содержащиеся в .x файле, при
помощи функции ParseObject. Когда придет время обрабатывать объект Mesh,
вместо вызова функции D3DXLoadMeshFromXof для загрузки данных каркаса
необходимо использовать функцию D3DXLoadSkinMeshFromXof, которая имеет
дополнительный параметр - указатель на объект ID3DXSkinInfo.
24

Проверить наличие костей позволяет вызов функция ID3DX-


SkinInfo::GetNumBones. Эта функция возвращает количество костей,
загруженных из шаблона Mesh. Если количество костей 0, то тогда их нет, и
можно освобождать объект ID3DXSkinInfo (используя Release). Если же
кости существуют, то можно продолжать использование скелетного каркаса.
После того как был создан скелетный каркас, необходимо создать второй
контейнер каркаса. Загруженный с помощью функции
D3DXLoadSkinMeshFromXof объект скелетного каркаса является основной
ссылкой на данные вершин каркаса. Так как эти вершины правильно
расположены в соответствии с расположением костей, все вершины придут в
неправильные позиции, если начать изменять эти положения.
Для решения данной проблемы необходимо создать второй объект каркаса
(ID3DXMesh), который будет содержать точную копию скелетного каркаса.
Необходимо считать данные вершин из скелетного каркаса, наложить
разнообразные преобразования костей и записать результирующие данные
вершин в дублирующий контейнер, который используется при визуализации.
Вторичный каркас в точности совпадает со скелетным каркасом, начиная от
количества вершин и заканчивая индексами. Самым простым методом
копирования объекта скелетного каркаса является использование функции
ID3DXMesh::CloseMeshFVF библиотеки DirectX.
При детальном просмотре .x файлов, можно заметить сходство объектов
Frame и SkinWeights. Для каждой кости в скелетной структуре есть
соответствующий объект SkinWeights, встроенный в объект Mesh, который
содержит имя объекта Frame (или ссылку Frame).
После загрузки скелетного каркаса, необходимо присоединить каждую кость
к соответствующему фрейму (рисунок 2.6). Это делается простым перебором
всех костей, получая имя каждой из них и находя соответствующий ей фрейм.
Каждый соответствующий указатель на фрейм хранится в специальной
структуре костей D3DXMESHCONTAINER_EX.
Структура D3DXMESHCONTAINER_EX добавляет к структуре
D3DXMESHCONTATNER, содержащейся в DirectX API, массив объектов текстур,
контейнер вторичного каркаса и данные прикрепления костей.
Массив ppFrameMatrices содержит преобразования из иерархии костей; одна
матрица преобразования накладывается на каждую вершину,
принадлежащую соответствующей кости. Единственной проблемой является
то, что матрица преобразования костей хранится не в виде массива, а отдельно
для каждой кости в иерархии.
Структура D3DXMESHCONTAINER_EX предоставляет указатель для
каждой матрицы преобразования костей, содержащейся в иерархии объектов
D3DXFRAME_EX в массиве указателей (ppFrameMatrices). Используя эти
указатели, возможно поместить каждое преобразование в созданный в результате
обновления скелетного каркаса массив pBoneMatrices.
25

Рисунок 2.6 – Иерархия костей, окруженная простейшим каркасом

В ходе разработки библиотеки по работе со скелетной анимацией были созданы


массивы указателей и матриц после загрузки иерархии костей, согласно
количеству костей в иерархии, тем самым были созданы указатели и объекты
D3DXMATRIX.
После загрузки скелетного каркаса, можно установить указатели на каждое
преобразование костей, получив имя каждой из них из информационного
объекта скелетного каркаса. Используя эти имена, можно просмотреть
список фреймов на их совпадения. Для каждой кости, у которой совпадает
имя, установить указатель на матрицу преобразования фрейма. Когда
все кости и фреймы проверены, можно перебрать весь список и
скопировать матрицы в массив pBoneMatrices.
Когда скелетная структура находится в желаемой позе, пришло время
обновить (или восстановить) скелетный каркас, чтобы он соответствовал ей.
Прежде чем восстанавливать скелетный каркас, необходимо убедиться, что
контейнер вторичного каркаса создан, и иерархия фреймов обновлена.
Для обновления скелетного каркаса необходимо сначала заблокировать
буферы вершин скелетного и вторичного каркаса. Это является очень
важным, потому что DirectX возьмет данные вершин скелетного каркаса,
наложит преобразования костей и запишет результирующие данные
вершин в объект вторичного каркаса.
Однако сначала необходимо скопировать преобразования их фреймов в
массив матриц (pBoneMatrices), хранимый в контейнере каркасов. В то же
самое время необходимо скомбинировать преобразования с обратным
преобразованием костей. Обратное преобразование костей ответственно
за перемещение вершин каркаса к его начальному положению, до того как
применяятся фактические преобразования.
26

Для применения преобразования к какому либо фрейму необходимо


передвинуть вершины, принадлежащие фрейму, в начальное положение, а
потом накладывать преобразования.
Необходимо перемещать вершины относительно начального положения
каркаса, прежде чем применять преобразования, так как матрица вращения
просто поворачивает вершины относительно начального положения. Если
вращать вершину, принадлежащую кости, вершина бы вращалась
относительно начального положения каркаса, а не соединения костей.
После передвижения вершин к центру каркаса накладывается
преобразование (чтобы поворот вершин соответствовал повороту костей) и,
наконец, преобразовывается в требуемое положение.
Обычно, преобразования костей обратной матрицы сохраняются в .x файле
при использовании программы трехмерного моделирования для создания
каркасов. Если же это информация не содержится в .x файле, то возможно ее
вычислить, сначала обновив иерархию фреймов, после чего обратив
комбинированное преобразование фреймов, используя функцию
D3DXMatrixInverse.
Однако вместо того чтобы вычислять все эти матрицы обратного
преобразования костей самостоятельно, возможно использовать объект
скелетного каркаса для получения этой информации. Вызвав функцию
ID3DXSkinInfo::GetBoneOffsetMatrix, можно получить указатель на обратную
матрицу преобразования кости. Умножив эту матрицу на матрицу
преобразования фрейма, получен необходимый результат.
Таким образом, необходимо перебрать все кости, получить обратное
преобразование, скомбинировать его с преобразованием фрейма и сохранить
результат в массиве pBoneMatrices.
После копирования преобразования костей в массив pBoneMatrices, можно
обновлять скелетные каркасы, сначала заблокировав буферы вершин для
скелетного и вторичного каркаса.
После блокирования буферов вершин, необходимо вызвать функцию
ID3DXSkinInfo::UpdateSkinnedMesh, чтобы наложить все преобразования
костей на вершины и записать результирующие данные в контейнер
вторичного каркаса. Затем следует просто разблокировать буферы вершин.
Следующим этапом является визуализация.
Для визуализации просто необходимо использовать функции отрисовки
обычных каркасов для визуализации вторичного каркаса. Перебрав все
материалы, необходимо установить материал и текстуру и вызвать функцию
ID3DXMesh::DrawSubset. Все вышеперечисленные действия необходимо
выполнять, пока все наборы не будут отрисованы.
Каждая кость в анимации имеет собственный список используемых ключей,
который хранятся в шаблоне Animation. (рисунок 2.7) Для каждой кости в
иерархии есть соответствующий объект Animation. Соответствующий класс
анимации содержит имя кости, к которой он присоединен, количество ключей
каждого типа (перемещения, масштабирования, вращения и преобразования),
указатель на связанный список данных и указатель на структуру кости (или
27

фрейма), используемую в иерархии. Также создан конструктор и деструктор,


который очищает данные класса.

Рисунок 2.7 – Анимация, основанная на ключевых кадрах

Наконец, шаблон AnimationSet содержит объект Animtation для всей иерархии


костей. Все, что на данный момент требуется от класса набора анимаций, -
это следить за массивом классов cAnimation (каждая кость в иерархии имеет
соответствующий ей класс cAnimation) и за длиной полной анимации.
Предположив, что требуется загрузить за раз больше одного набора анимации,
возможно создать класс, содержащий массив (или даже связанный список)
классов cAnimationSet, означая, что возможно получить доступ ко всем
анимациям используя один интерфейс. Этот класс, называемый
cAnimationCollection, наследуется от разработанного класса cXParser, так что
можно анализировать .x файлы напрямую из класса, хранящего анимации.
Специализированный анализатор .x файлов, содержащийся в классе
cAnimationCollection, выполняет следующее - загружает данные из объекта
Animation в массив объектов.
Для каждого объекта AnimationSet, найденного в анализируемом .x файле,
необходимо создать класс cAnimationSet и добавить его к связанному списку
уже загруженных наборов анимации. Последний загруженный объект
cAnimationSet хранится в начале связанного списка, что облегчает
определение, данные какого набора используются в данный момент.
Далее возможно соответствующим образом анализировать объекты
Animation. Если сохранять последний загруженный объект cAnimationSet в
начале связанного списка, каждый следующий анализируемый объект
Animation будет принадлежать текущему набору анимаций. То же самое и для
объектов AnimationKey - их данные будут принадлежать первому объекту
cAnimation в связанном списке.
В классе cAnimationCollection разработаны функции, важнейшая из которых -
cAnimationCollection::ParseObject, - имеет дело с каждым объектом,
анализируемым в .x файле.
Функция ParseObject начинается с проверки, является ли текущий перечисляемый
объект объектом AnimationSet. Если да, то новый объект cAnimationSet
28

размещается и привязывается в список объектов, в то же время объект набора


анимаций именуется для дальнейшего использования.
Далее просто создается объект, который будет содержать данные объекта
Animation. Затем анализируется объект Animation. Так как речь идет об
объекте cAnimation, функция получает экземпляр фрейма, расположенного в
объекте Animation. В объекте Animation разрешены только ссылочные
объекты фрейма, которые возможно проверить, используя GUID
родительского шаблона.
Оставшийся код функции ParseObject определяет тип данных ключа,
содержащегося в объекте AnimationKey. Код разделяется в зависимости от
типа данных и считывает данные в заданные объекты ключей (m_RotationKeys,
m_TranslationKeys, m_ScaleKeys и m_MatrixKeys), находящиеся в текущем
объекте cAnimation.
После загрузки данных анимации необходимо прикрепить классы анимаций к
соответствующим им костям в иерархии костей. Сопоставление иерархий
имеет большое значение, так, как только анимация обновляется, необходимо
быстро получить доступ к преобразованиям костей. Сопоставив, можно
получить простой метод доступа к костям.
В данной работе иерархия костей представлена иерархией D3DXFRAME.
Структура D3DXFRAME использует два связанных списка указателей,
которые применяются для создания иерархии. Из корневой структуры
D3DXFRAME можно получать доступ к дочерним объектам, используя
указатель D3DXFRAME::pFrameFirstChild, и к родственным объектам,
используя указатель D3DXFRAME::pFrameSibling.
Следующая функция в классе cAnimationCollection, на которую необходимо
обратить внимание, - Map. Она используется для прикрепления указателя
m_Bone анимационной структуры к фрейму, имеющему в иерархии фреймов
то же самое имя.
Функция Map просматривает все объекты cAnimationSet и находящиеся в них
объекты cAnimation. Название каждого объекта cAnimation сравнивается с
названием каждого фрейма; если найдено совпадение, в указатель
cAnimation::m_Bone устанавливается адрес фрейма.
Функция Map имеет в качестве параметра корневой фрейм иерархии.
В то время как функция Map только просматривает все объекты cAnimationSet
и cAnimation, функция FindFrame рекурсивно обрабатывает иерархию фреймов в
поисках соответствия заданному имени. Когда такое имя найдено, функция
FindFrame возвращает указатель на найденный фрейм.
Данные анимации, таким образом, загружены, и объекты анимации
прикреплены к иерархии костей.
После прикрепления классов анимации к иерархии костей можно начать
анимировать каркасы. Все, что необходимо сделать, - это просмотреть ключи
анимации для каждой кости, накладывая интерполированные преобразования
на преобразования костей перед визуализацией. Это было сделано простым
перебором каждого класса анимации и его ключей для нахождения
используемых значений ключа.
29

Вернувшись к классу cAnimationCollection, можно увидиь, что всего одна


функция выполняет всю эту работу. Предоставив функции
cAnimationCollection::Update в качестве параметра название используемого
набора анимаций и время в анимации, все матрицы преобразования во всей
иерархии прикрепленных костей будут установлены и готовы к
визуализации.
Функция Update начинает работу с просмотра наборов анимаций, загруженных
в связанный список. Если установить значение NULL в качестве
AnimationSetName, Update просто будет использовать первый набор анимаций в
списке (который обычно является последним загруженным). Если при
использовании заданного названия не было найдено совпадений, функция
прекращает работу.
Однако, как только найден соответствующий набор анимаций, функция
продолжает работу, просматривая каждый объект cAnimation в нем. Для
каждого объекта анимации ищется ключ (перемещения, масштабирования,
вращения и преобразования), который необходимо использовать для заданного
значения времени.
После того как подходящий ключ обнаружен, значения (вращения,
масштабирования, перемещения или преобразования) интерполируются и
вычисляется матрица результирующего преобразования. Далее эта матрица
хранится в прикрепленной кости (на которую указывает указатель m_Bone).
Таким образом был разработан компонент СSkinnedMesh который позволяет
путем вызова минимальных методов отображать и обновлять анимированный
объект.

2.6 Шейдеры

Для работы с шейдерами в ядро был включен разработанный класс


CElekyShader, который представляет собой оболочку, необходимую для
загрузки и использования при визуализации файлов-шейдеров, тем самым
добавляя в сцену необходимые эффекты.
После разработки и отладки шейдерной программы необходима ее
интеграция в конечный используемый продукт. Такими продуктами могут
быть приложения для обработки графики, написанные на языке C++ с
использованием библиотеки DirectX (например, компьютерные игры).
Одним из простейших путей использования шейдерных программ является
следующий: файл с программой – эффектом хранится в отдельном файле на
некотором носителе, программа динамически подгружает его в нужный
момент, и шейдерный код сможет выполниться в требуемый момент
визуализации сцены.
Первый шаг для работы с шейдерной программой – создание шейдерного
эффекта с использованием функции D3DXCreateEffectFromFile(). Функция
принимает необходимое количество параметров, среди которых: указатель на
путь к имени файла с шейдерной программой, указатель на устройство, на
котором необходимо работать с эффектом, указатель на интерфейс, в
котором будет создан эффект, и другие.
30

Подразумевая, что загрузка файла эффектов прошла успешно, возможно


использовать другие функции API для установки всех необходимых для
шейдера переменных. Например, возможно использование функции
SetMatrix() интерфейса ID3DXEffect для установки матрицы преобразования
matWorldViewProj и других. Функция принимает в качестве первого
параметра имя устанавливаемой переменной в шейдерной программе, второй
параметр – указатель на матрицу текущей программы. Существуют у
интерфейса ID3DXEffect и соответствующие функции для установки
вещественных переменных и векторов (SetFloat() и SetVector()
соответственно), принимающие в качестве параметров аналогичные функции
SetMatrix(). Установка текстур для шейдера также аналогична
вышеперечисленным функциям и осуществляется функцией SetTexture()
интерфейса ID3DXEffect. Пример установки переменных представлен в
листинге 2.4.

Листинг 2.4 – Установка переменных-констант


m_pEffect->SetMatrix ("matWorldViewProj", &m_matWorldViewProj);
m_pEffect->SetMatrix ("matWorldView", &m_matWorldView);
m_pEffect->SetFloat ("noiseFrequency ", &m_fNoiseFreq);
m_pEffect->SetVector("g_Leye", &g_Leye);
m_pEffect->SetTexture("tVolumeNoise", m_pVolumeNoiseTexture);

После установки всех необходимых для работы программы-шейдера


переменных необходимо установить технику используемого эффекта
(определена в файле эффектов в разделе technique). Установка
осуществляется функцией SetTechnique() интерфейса ID3DXEffect. Для
установки техники может потребоваться вспомогательная функция
интерфейса GetTechniqueByName(), получающая в качестве параметра
название техники, определенной в файле программы-шейдера, и
возвращающая дескриптор техники, необходимый для функции
SetTechnique(), который является единственным и необходимым для нее
параметром.
Для визуализации объекта с использованием эффекта, необходимо вызвать
функцию Begin() интерфейса ID3DXEffect, получающий в качестве первого
параметра указатель на переменную целого типа, в которую будет занесено
количество проходов, необходимое для выполнения техники используемого
эффекта. Обработка одного прохода реализуется функцией Pass() интерфейса
ID3DXEffect, получающей в качестве параметра номер прохода. После
вызова функции Pass() выполняется визуализация геометрии необходимого
объекта или нескольких объектов одним из обычных для библиотеки
Direct3D способов. После завершения всех проходов, необходимо вызвать
функцию End() интерфейса ID3DXEffect, которая завершит обработку
эффекта. В листинге 2.5 показан пример визуализации объекта с
использованием шейдеров.

Листинг 2.5 – Визуализация с использованием шейдеров


UINT cPasses;
m_pEffect->Begin(&cPasses, 0);
31

for (int iPass = 0; iPass < cPasses; iPass++)


{
m_pEffect->Pass(iPass);
// Отрисовка геометрии
}
m_pEffect->End();

Как можно увидеть, процесс обработки графики с использованием шейдеров


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

2.7 Работа с камерой

В ходе разработки ядра графического приложения было включено два


компонента работы с камерой. Первый из них (CElekyCamera) обеспечивает
взаимодействие с камерой как со свободной, второй – как с камерой от
третьего лица (CEleky3rdPersonCamera).
Камера определяет какую часть мира может видеть зритель и, следовательно,
для какой части мира необходимо создавать ее двухмерное изображение.
Камера позиционируется и ориентируется в пространстве и определяет
видимую облать пространства. Схема используемой модели камеры показана
на рисунке 2.8.
Область видимого пространства преставляет собой усеченную пирамиду
(frustum) и определяется углами поля зрения, передней и задней плоскостями.
Причины использования усеченной пирамиды станут ясны, если принять во
внимание, что экран на котором отображается сцена — прямоугольный.
Объекты, которые не находятся внутри заданного пространства невидимы и
должны быть исключены из процесса дальнейшей обработки. Процесс
исключения таких данных называется отсечением (clipping).
Окно проекции (projection window) — это двухмерная область на которую
проецируются находящиеся внутри области видимого пространства
трехмерные объекты для создания двухмерного изображения,
представляющего трехмерную сцену. Важно помнить, что окно проекции
определяются таким образом, что координаты его правого верхнего угла
будут (1, 1), а координаты левого нижнего угла – (–1, –1).
32

Рисунок 2.8 – Модель камеры

Определение местоположения и ориентации камеры относительно мировой


системы координат осуществляется с помощью векторов камеры (camera
vectors): верхнего вектора (up vector), вектора взгляда (look vector) и вектора
местоположения (position vector). Эти векторы образуют локальную систему
координат камеры, описанную в мировой системе координат.
Первый из разработанных классов камеры содержит в себе следующие
возможности: вращение камеры относительно любой из осей, перемещение
камеры в указанную позицию координатами X, Y и Z, перемещение вперед
/назад на указанное расстояние и влево/вправо. В листинге 2.6 представлен
код, который обновляет камеру каждый раз, когда необходимо ее изменить.

Листинг 2.6 – Обновление камеры


void CElekyCamera::Update()
{
pos = D3DXVECTOR3 ( 0.0f, 0.0f, 0.0f);
targ = D3DXVECTOR3 ( 0.0f, 0.0f, 1.0f);
up = D3DXVECTOR3 ( 0.0f, 1.0f, 0.0f);

D3DXMATRIX matpos;
D3DXMatrixTranslation (&matpos, 0.0f, landHeight, 0.0f);

matView = matViewX*matViewY*matViewT;
matView *= matpos;

D3DXVec3TransformCoord (&pos, &pos, &matView);


D3DXVec3TransformCoord (&targ, &targ, &matView);
D3DXVec3TransformNormal (&up, &up, &matView);

D3DXMatrixLookAtLH (&matView, &pos, &targ, &up);


pd3dDevice->SetTransform (D3DTS_VIEW, &matView);
}
33

Класс камеры с видом от третьего лица содержит всего два метода по


изменению ее ориентации – это метод, который позволяет вращать камеру по
оси X относительно объекта и метод, который позволяет удалять камеру от
объекта на заданное расстояние. Также в классе присутствует метод update
(листинг 2.7), который обновляет камеру в соответствии с переданными ему
координатами и значениями косинуса и синуса угла поворота относительно
оси Y. Данная функция принимает значения и синуса и косинуса, что не
совсем логично в силу оптимизации быстродействия, так как значения
синуса и косинуса могут быть получены из объекта CElekyMesh, к которому
чаще всего привязывается камера.

Листинг 2.7 – Обновление камеры с видом от третьего лица


void CEleky3rdPersCamera::Update(D3DXVECTOR3 vtarg, float cosa, float sina)
{
pos = D3DXVECTOR3 ( vtarg.x - r * sina, vtarg.y + r * sinf(h), vtarg.z -
r * cosa);
targ = vtarg;
D3DXMatrixLookAtLH (&matView, &pos, &targ, &up);
pd3dDevice->SetTransform (D3DTS_VIEW, &matView);
}

2.8 Вспомогательные возможности

При разработке ядра графического приложения в него были включены такие


дополнительные возможности, как определение количества кадров в секунду
и скорости анимации. Это одни из необходимых при разработке анимации
основанной на ключевых кадрах функций. Эти функции обеспечивает
компонент CElekyTiming, который хранит переменные fRate – время за
которое был отображен один кадр анимации и fFPS – количество кадров
анимации в секунду.
Также одним из немаловажных компонентов является компонент управления
вводом пользователя. Данный компонент основан на компоненте библиотеки
DirectX – DirectInput. Он позволяет определять состояние мыши и
клавиатуры пользователя для обеспечения ориентирования в виртуальном
мире.

2.9 Оптимизация

Одной из важнейших функций ядра любой видеоигры является функция


оптимального рисования графических объектов в сцене. В процессе
обработки трехмерной графики и построения приложения возникает
необходимость выборочного отображения объектов, так как прямая отсылка
всех трехмерных объектов в конвейер визуализации резко ведет к потере
производительности в целом. Технология, позволяющая отсеивать ненужные
для рисования объекты носит название «отсечение по видимому объему». На
рисунке 2.6 представлен видимый объем, который ограничен передней
плоскостью, задней плоскостью и плоскостями, которые находятся между
34

прямыми, соединяющими углы данных плоскостей. (Здесь и далее сделано


допущение для упрощения понимания, что плоскость представляет собой
прямоугольник, позиционированный в плоскости в математическом
определении).
Пирамида видимости – это часть пространства, в которой находятся все
объекты, видимые из данной точки в данный момент. Она определяется
шестью гранями усеченной пирамиды (т.е. пирамиды со срезанной
вершиной). Если какая-то точка находится внутри пирамиды видимости, ее
видно. Если вне пирамиды, значит, эту точку не видно.
Видимый объем представляет собой усеченную пирамиду, грани которой
являются плоскостями, найти которые (их уравнения) возможно используя
матрицу вида и проекционную матрицу (листинг 2.8)

Листинг 2.8 – Нахождение плоскостей усеченной пирамиды


D3DXMATRIX matViewProj;
D3DXMATRIX matView, matProj;
pd3dDevice->GetTransform(D3DTS_VIEW, &matView);
pd3dDevice->GetTransform(D3DTS_PROJECTION, &matProj);
matViewProj = matView * matProj;
vFrustum[0].a = matViewProj._14 + matViewProj._11;
vFrustum[0].b = matViewProj._24 + matViewProj._21;
vFrustum[0].c = matViewProj._34 + matViewProj._31;
vFrustum[0].d = matViewProj._44 + matViewProj._41;
// Right plane
vFrustum[1].a = matViewProj._14 - matViewProj._11;
vFrustum[1].b = matViewProj._24 - matViewProj._21;
vFrustum[1].c = matViewProj._34 - matViewProj._31;
vFrustum[1].d = matViewProj._44 - matViewProj._41;
// Top plane
vFrustum[2].a = matViewProj._14 - matViewProj._12;
vFrustum[2].b = matViewProj._24 - matViewProj._22;
vFrustum[2].c = matViewProj._34 - matViewProj._32;
vFrustum[2].d = matViewProj._44 - matViewProj._42;
// Bottom plane
vFrustum[3].a = matViewProj._14 + matViewProj._12;
vFrustum[3].b = matViewProj._24 + matViewProj._22;
vFrustum[3].c = matViewProj._34 + matViewProj._32;
vFrustum[3].d = matViewProj._44 + matViewProj._42;
// Near plane
vFrustum[4].a = matViewProj._13;
vFrustum[4].b = matViewProj._23;
vFrustum[4].c = matViewProj._33;
vFrustum[4].d = matViewProj._43;
// Far plane
vFrustum[5].a = matViewProj._14 - matViewProj._13;
vFrustum[5].b = matViewProj._24 - matViewProj._23;
vFrustum[5].c = matViewProj._34 - matViewProj._33;
vFrustum[5].d = matViewProj._44 - matViewProj._43;

Как видно из приведенного выше кода, все грани пирамиды представлены


уравнениями плоскостей, описанными вектором нормали и расстоянием до
плоскости. Все векторы нормали каждой плоскости направлены внутрь
пирамиды, поэтому определение принадлежности точки пирамиде не
вызывает труда и выполняется путем подставления координат точки в
формулу a*x+b*y+c*z+d и вычисления знака результата. Результат должен
35

быть положительным для каждой плоскости, если точка лежит внутри


пирамиды.
Перебор всех вершин для каждого объекта не является оптимальным путем и
займет даже больше времени чем вывод всех объектов в графический
конвейер. Одним из путей решений этой проблемы является вычисление
ограничивающего объема для каждого объекта. Ограничивающий объем –
это параллелепипед, описанный вокруг трехмерного объекта. Так, для
выяснения попадает ли объект внутрь пирамиды видимости, достаточно
чтобы хотя бы одна из точек ограничивающего объема попадала в эту
пирамиду. Для каждого объекта количество проверок уменьшается максимум
до 8 точек. Еще одним из путей оптимизации является создание не
ограничивающего объема, а ограничивающей сферы, так количество точек
для проверки уменьшается до одной. Листинг 2.9 демонстрирует проверку
попадания ограничивающей сферы в пирамиду видимости.

Листинг 2.9 – Проверка попадания сферы в пирамиду видимости


bool SphereInFrustum( float x, float y, float z, float fRadius )
{
for(int p = 0; p < 6; p++ )
if( vFrustum[p].a * x + vFrustum[p].b * y + vFrustum[p].c * z +
vFrustum[p].d <= -fRadius )
return false;
return true;
}

Здесь можно сделать дополнительную оптимизацию: возвращать из функции


не значение true или false, а 0 в случае непересекаемости ограничивающей
сферы и пирамиды видимости и vFrustum[p].a * x + vFrustum[p].b * y +
vFrustum[p].c * z + vFrustum[p].d + fRadius для получения расстояния от
камеры до объекта (листинг 2.10).

Листинг 2.9 – Проверка попадания сферы в пирамиду видимости


float SphereInFrustum( float x, float y, float z, float radius )
{
float d;
for(int p = 0; p < 6; p++ )
{
d = frustum[p][0] * x + frustum[p][1] * y + frustum[p][2] * z +
frustum[p][3];
if( d <= -radius )
return 0;
}
return d + radius;
}

Этот метод не отличается от предыдущего, только возвращает 0, если сфера


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

много полигонов, а если он далеко, то обойтись и менее детальным


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

Рисунок 2.9 – Квадродерево

Мир (представляемый как куб, заключающий в себе все полигоны,


описывающие сцену) разделяется на меньшие узлы равного размера.
Квадродерево разделяет узлы в двухмерном пространстве (используя оси X и
Z), а октодерево разделяет узлы в трехмерном пространстве (используя все
оси). Последнее актуально использовать при расположении трехмерных
объектах в нескольких уровнях.
Узел представляет группу полигонов и в то же время представляет область
трехмерного пространства. Каждый узел связан с другими отношениями
родитель-потомок. Это означает, что узел может содержать другие узлы, и
каждый последующий узел является частью, меньшей чем его родитель. Весь
трехмерный мир в целом считают корневым узлом (root node) — самым
верхним узлом, с которым связаны все другие узлы.
При построении дерева каждый объект сцены заключается в
ограничивающую область, которая определяет протяженность объекта по
всем направлениям. Если ограничивающая область находится в узле
трехмерного пространства (полностью или частично), значит объект
относится к узлу. Объект может относиться к нескольким узлам, поскольку
протяженность объекта также может распространяться на несколько узлов.
Построив древовидную структуру необходимо визуализировать ее. Начав с
корневого узла, выполняется проверку попадания узла в пирамиду видимого
пространства. Узел считается видимым, если хотя бы одна из восьми точе,
37

формирующих его в трехмерном пространстве, находится внутри пирамиды


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

3 РАЗРАБОТКА СИМУЛЯТОРА ДОРОЖНОГО ДВИЖЕНИЯ


На основе разработанного ядра графического приложения был разработан
симулятор дорожного движения, который использует все функции
графического ядра и предназначен как для развлекательных целей, так и для
демонстрации возможностей разработанных библиотек.
Программа-симулятор дорожного движения представляет собой графическое
приложение, использующее трехмерную графику, построенное на основе
библиотеки DirectX. Разработанная программа содержит в себе следующие
возможности:
– загрузка и отображение трехмерного ландшафта из raw файла;
– управление камерой пользователя с возможностью переключения между
свободной камерой и камерой, привязанной к трехмерному объекту.
– построение сети дорог на основе объектов участков дорог и карт
расположения;
– загрузка и расположение статических объектов (домов, деревьев и других)
из файлов .x на основе карт расположения;
– отображение двухмерного интерфейса пользователя, предоставляющего
информацию о текущем состоянии сцены;
– загрузка и автоматическая манипуляция объектов движения (машин) по
карте дорог в соответствии с картой движения;
– загрузка и автоматическая манипуляция объектов движения (машин) по
карте дорог случайным образом;
– загрузка и манипуляция при помощи ввода пользователя объекта движения
(машины) по карте дорог;

Рисунок 3.1 – Основной интерфейс программы


39

– управление трафиком при помощи светофоров и автоматическое


предотвращение аварийных ситуаций;
– управление аркадной составляющей.
В основе программы лежит разработанное ядро и все функции по работе с
графическими объектами реализуется путем обращения к его функциям.
Основной интерфейс программы представляет (рисунок 3.1) представляет
окно, ограниченное панелями интерфейса, на которых отображается
информация о текущем игровом процессе. Основная часть окна отображает
трехмерную графическую сцену, управляемую и анимируемую как
автоматически, так и при помощи ввода пользователя.

3.1 Построение ландшафта и управление камерой

В основе игровой сцены лежит трехмерный ландшафт сгенерированный из


карты высот (рисунок 3.2), центр которого расположен в точке (0, 0, 0).
Работа с ландшафтом выполняется полностью путем обращений к функциям
разработанного ядра. В качестве текстуры ландшафта использована простая
текстура травы (рисунок 3.2), координаты которой присвоены каждой из
граней сетки трехмерного ландшафта. Листинг 3.1 демонстрирует
возможности изменения высоты для конкретных точек ландшафта.

Листинг 3.1 – Изменение высот в определенных вершинах ландшафта


pTerrain->setHeightmapEntry(102, 99, 15);
pTerrain->setHeightmapEntry(102, 100, 15);
pTerrain->setHeightmapEntry(103, 99, 15);
pTerrain->setHeightmapEntry(103, 100, 15);

Рисунок 3.2 Карта высот и текстура для ландшафта

Данная возможность может быть полезной при необходимости выравнивания


определенной области ландшафта по определенной высоте, а также для
создания искусственных перепадов высот, не связанных с картой высот,
загруженной из raw файла. Raw файл для разработанной программы
представляет собой графический восьмибитный файл, размером 200 на 200
пикселей, созданный в программе Adobe Photoshop, имеет плавные переходы
40

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


поверхности.
Для перемещения по поверхности ландшафта использованы классы
CElekyCamera и CEleky3rdPersonCamera разработанного ядра. Переключение
между камерами осуществляется через кнопки 1 и 2 клавиатуры.
Информация о текущей камере и ее позиции отображается в основном
интерфейсе программы. Рисунок 3.3 демонстрирует сгенерированный из
карты высот ландшафт с наложенной на него текстурой, представленной на
рисунке 3.2 в позиционированной в нем камере.

Рисунок 3.3 – Построенный ландшафт

При разработке ядра графического приложения была разработана


возможность получить высоту ландшафта в любой заданной координате
(X,Z), таким образом, позиционирование камеры осуществляется в
соответствии с вводом пользователя по координатам (X,Z) и получением
высоты Y из класса CElekyTerrain в соответствии с заданными координатами.
Управление камерой осуществляется путем нажатия кнопок клавиатуры W –
для перемещения вперед, S – для перемещения назад, A – для перемещения
влево и Z для перемещения вправо. При обработке ввода пользователя в
методе input() класса CEleky вызывается соответствующая функция
свободной камеры - MoveForward и Strife соответственно. Вращение камеры
осуществляется при помощи перемещения мыши вверх/вниз и влево/вправо.
Перемещение мыши влево/вправо вращает камеру вокруг оси Y, вызывая
функцию RotateY класса CElekyCamera, а перемещение мыши вверх/вниз
вращает камеру вокруг оси X, вызывая функцию RotateX.
41

3.2 Построение сети дорог

В основе разработанной программе лежит сеть дорог, позиционированная на


ландшафте в соответствии с картой дорог. Для работы с сетью дорог
разработан класс CPassRoads, содержащий основные методы init, update и
render. Для работы данного класса требуется загрузка четырех объектов типа
ID3DXMesh из .x файлов. Данные объекты представляют собой
определенный участок дорог. При разработке симулятора дорожного
движения было сделано допущение, что дороги могут располагаться на
ландшафте только по направлениям сетки ландшафта и не пересекают ее.
Так, для реализации любой дороги необходимы следующие объекты: прямой
участок дороги, Г-образный участок дороги, Т-образный участок дороги и Х-
образный участок дороги. Данные участки могут быть смоделированы в
любом 3-х мерном графическом редакторе. Для разработки объектов для
симулятора дорожного движения был выбран редактор 3ds max (рисунок
3.4).

Рисунок 3.4 – Участки дорог

Каждый из участков дорог имеет размеры 10 на 10, так как каждая ячейка
ландшафта имеет размеры 10 на 10. При разработке было принято, что одна
ячейка ландшафта соответствует размерам 10 на 10 метров реального мира.
Принимать определенный масштаб при разработке графических приложений
является хорошей практикой, потому что позволяет впоследствии
разрабатывать графические объекты соответствующих пропорций.
Метод init класса CPassRoads загружает последовательно объекты участков
дорог, затем читает файл карты дорог, который представляет набор
координат, перечисленных через пробел. Формат файла карты дорог имеет
следующую структуру:
N
X1 Z1 T1
X2 Z2 T2
42


XN ZN TN
где N – количество участков дорог, X и Z – координаты участка дорог с
округлением до 10 (так как каждый участок распложен в центре ячейки
ландшафта), T – тип участка дороги.
Типы участка дороги могут быть следующими: 1 – вертикальный участок, 2 –
горизонтальный участок, 3 – поворот с вертикального участка на 90 градусов
вправо, 4 – поворот с горизонтального участка на 90 градусов влево, 5 –
поворот с горизонтального участка на 90 градусов вправо, 6 – поворот с
вертикального участка на 90 градусов влево, 13-16 – Т-образные перекрестки,
17 – X-образный перекресток (рисунок 3.5).

Рисунок 3.5 – Схема типов участков дорог

При разработке было принято решение о том, что участки дорог с типами 3-
17 могут лежать только горизонтально (иметь угол поворота относительно
оси X ноль градусов), поэтому после загрузки сети дорог необходимо
выровнять карту высот и соответственно ландшафт на участках с типами 3-
17. Для того чтобы выровнять ландшафт по карте дорог, необходимо
отсортировать по убыванию высоты тайтлы, установив сначала те, которые
выше. Выравнивание ландшафта по карте дорог и установка углов поворота
по оси X для дороги типа 1-2 производится в функции update, вызываемой
для каждого из участков (листинг 3.2).

Листинг 3.2 – Обновление участков дорог


void CPassRoad::update(int i)
{
float h, h2;
if(vRoads[i].type == 1)
{
// получение максимальной высоты из четырех точек ландшафта
...
}
else if(vRoads[i].type == 2)
{
// получение максимальной высоты из четырех точек ландшафта
...
}
else // T and X crosses
{
// выровнять ландшафт
h = getMaxTerHeight(i);

pTerrain->setHeightmapEntry(vRoads[i].y, vRoads[i].x, (int)h);


pTerrain->setHeightmapEntry(vRoads[i].y + 1, vRoads[i].x, (int)h);
pTerrain->setHeightmapEntry(vRoads[i].y, vRoads[i].x + 1, (int)h);
pTerrain->setHeightmapEntry(vRoads[i].y+1,vRoads[i].x + 1,(int)h);
43

h2 = h;
}

float r = fabs(h2-h);
float sgn = h2 > h ? -1.0f : 1.0f;
float a = (float)atan( r / fCellSize);
pStraightMesh->IdentityMatrices();
pStraightMesh->SetScale(1.0f, 1.0f, sqrtf(fCellSize * fCellSize + r * r)
/ fCellSize);
pStraightMesh->RotateX(sgn*a);
if(vRoads[i].type == 2)
pStraightMesh->RotateY( D3DX_PI / 2.0f );
if(vRoads[i].type == 3 || vRoads[i].type == 15)
pStraightMesh->RotateY( D3DX_PI / 2.0f );
if(vRoads[i].type == 4 || vRoads[i].type == 14)
pStraightMesh->RotateY( - D3DX_PI / 2.0f );
if(vRoads[i].type == 5 || vRoads[i].type == 13)
pStraightMesh->RotateY( D3DX_PI );
pStraightMesh->SetPosition((vRoads[i].x-((pTerrain->nCols-1)/2.0f)) *
fCellSize,
((h+h2)/2.0f),
(vRoads[i].y - ((pTerrain->nRows - 1) /
2.0f)) * fCellSize);
pStraightMesh->Update();
pMatWorld[i] = pStraightMesh->GetMatWorld();
}

В методе update строятся матрицы трансформаций для каждого объекта, а в


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

Рисунок 3.6 – Сцена после загрузки сети дорог.


Разработанная программа позволяет загружать сколь угодно много дорог из
разных карт дорог и разных объектов. Так в программе реализованы как
44

проселочные, так и дороги с улучшенным покрытием. Для реализации общей


сети дорог в одном объекте был разработан вспомогательный класс
CPassRoadCollection, который представляет для использующего класса
двумерный массив, каждая ячейка которого хранит тип расположенного в
нем участка дороги. Данный класс используется впоследствии при
перемещении объектов-машин по сети дорог. Условиями разработанных
классов, которые должен контролировать программист при разработке
являются следующие: дороги не должны пересекаться в неустановленных
местах, а также между дорогами, расположенными в разных картах дорог
должны быть как минимум промежутки в один квадрат ландшафта.
Соединяться между собой разные карты дорог могут только участками с
типами 1 или 2.

3.3 Статические объекты

В разработанном симуляторе дорожного движения статические объекты


представлены деревьями, зданиями, заборами и другими подобными
объектами. Статические объекты предназначены для декорирования
ландшафта и придания сцене заполненности. Рассматриваемые объекты были
смоделированы и текстурированы в программе 3ds max и размещены на
плоскости для наглядного моделирования виртуального города (рисунок 3.7).

Рисунок 3.7 – Моделирование виртуального города

При моделировании были сохранены координаты каждого объекта и


помещены в файл, имеющий следующий формат:
N
X1 Y1 R1 T1
X2 Y2 R2 T2
...
XN YN RN TN
45

где, N – количество объектов в файле, X и Y координаты объекта на


плоскости, R – угол поворота объекта относительно оси Y, Т – тип объекта,
представляющий название файла в формате .x.
Все статические объекты виртуального мира были разделены на три файла
определений. Первый файл хранит информацию о городских объектах,
второй о сельских и третий файл хранит информацию о положении деревьев
на карте, координаты которых сгенерированы случайным образом.
Статические объекты виртуального мира в целях оптимизации работают с
классом CElekyCulling, который обеспечивает отсечение объектов по
видимому объему. Так, сравнивая производительность прямого вывода всех
объектов и вывода объектов через отсечение и проверку по квадродереву,
можно заметить увеличение в последнем случае количество выводимых
кадров в секунду в 2 раза.
На рисунке 3.8 представлена сцена с отображенными на ней статическими
объектами.

Рисунок 3.8 – Сцена с отображенными статическими объектами

3.4 Перемещаемые объекты

Основная часть разработанной программы представлена перемещаемыми


объектами (автомобилями), которые могут перемещаться автоматически по
заданной траектории, автоматически, выбирая траекторию случайным
образом, и, наконец, путем управления пользователем.
Главный класс, реализующий перемещение автомобиля по заданной
траектории, CPassCar представляет интерфейс для быстрой загрузки модели
автомобиля, обновления его координат в соответствии с текущей скоростью
и отображения автомобиля в трехмерной сцене.
46

Класс CPassCar содержит методы init, update и render. Метод init вызывается
один раз только для загрузки модели автомобиля через класс CElekyMesh,
загрузки карты путей автомобиля и позиционирования автомобиля на дороге.
Карта путей автомобиля содержится в файле, имеющим следующую
структуру:
N
X1 Y1
X2 Y2
...
XN YN
XP YP ZP NP
где, N – количество ключевых точек, X и Y координаты каждой из ключевых
точек деленные на 10, чтобы соответствовать координатам высот на карте
высот. XP, YP, ZP – первоначальные координаты автомобиля в мировых
координатах. NP – первоначальное направление автомобиля (рисунок 3.6).
В каждой клетке ландшафта автомобиль имеет определенное направление,
хранимое в переменной-члене класса iDirOfCar, может принимать значения,
определяемые текущее направление автомобиля в пространстве (рисунок
3.9).

Рисунок 3.9 Направление автомобиля

Основная работа данного класса заключается в методе update. Данный метод


вызывается перед каждым обновлением кадра для перемещения автомобиля
в следующую позицию.
Работа функции update начинается аналогично функции update класса
CPassRoads с определения текущей высоты объекта по карте высот
ландшафта и определения необходимости поворота объекта автомобиля
относительно оси X. Данная трансформация необходима для определения
едет ли автомобиль вгору либо наспуск.
Среди переменных-членов класса CPassCar присутствует переменная,
определяющая текущее ускорение автомобиля fAccelerate, а также
переменная, определяющая текущую скорость fSpeed. Из формулы,
определяющей скорость в физике можно найти скорость автомобиля в любой
момент времени. Так как в разработанном ядре присутствует возможность
определения времени, за которое отобразился один кадр и которое прошло от
одного вызова update до второго. В разработанном классе так же
присутствует переменная, которая определяет максимальную скорость
автомобиля.
Для исключения возможностей столкновения автомобилей был разработан
класс CPassTitles, куда заносится информация от каждого из объектов-
автомобилей о текущей занимаемой ячейке. Если следующая по ходу
47

движения ячейка занята, то выполняется экстренное замедление автомобиля,


которое должно завершиться в течение движения вдоль одной ячейки.
Экстренное замедление автомобиля является ускорением в физическом
смысле и хранится в переменной fExtreme. Если переменная fExtreme не
равна 0, то переменная fAccelerate обнуляется принудительно (листинг 3.3)

Листинг 3.3 – Определение текущей скорости автомобиля


if(bExtremeSlow)
fAccelerate = 0;
else if(fFixedAccel >= 0.001f)
fAccelerate = fFixedAccel;

fSpeed = fSpeed + fExtreme_a * pTiming->rate();


fSpeed = fSpeed + fAccelerate * pTiming->rate();

if(fSpeed > fMaxSpeed)


fSpeed = fMaxSpeed;
else if(fSpeed <= 0.0f)
{
fSpeed = 0.0f;
fExtreme_a = 0.0f;
}
float fToMove = pTiming->rate() * fSpeed;

Для исключения возможности движения в обратном направлении


переменные скорости и ускорения обнуляются при достижении значения
скорости 0. Переменная fToMove хранит значение пройденного пути за
текущий промежуток времени. При достижении границы клетки происходит
переход в карте пути на следующую ключевую позицию. Граница клетки
определяется через размер клетки ландшафта. Однако может возникнуть
ситуация, когда длина клетки больше чем размер клетки ландшафта. Эта
ситуация появляется при движении автомобиля вгору или наспуск. В этом
случае длина клетки вычисляется из теоремы Пифагора (зная разницу высот
между крайними точками клетки и длину клетки ландшафта) (листинг 3.4).

Листинг 3.4 – Определение высоты, поворота и длины пути автомобиля


float r = fabs(h2-h); // средняя высота
float sgn = h2 > h ? -1.0f : 1.0f; // знак поворота (по часовой или против)
float a = (float)atan( r / fCellSize ); // угол поворота (из треугольника)
pCarMesh->RotateXFromZero( sgn * a ); // непосредственное вращение
if(fabs(h2 - h) < 0.0001f)
pCarMesh->SetHeight(h);
float fThisCellSize = sqrtf(fCellSize*fCellSize + r*r);// определение длины

При повороте (движении по криволинейной траектории) автомобиля


возникают некоторые трудности определения позиции автомобиля в каждый
момент времени. Для поворота необходимо определить дугу, по которой
движется автомобиль. Длина дуги может быть определена из математической
формулы и равна радиусу поворота умноженному на угол поворота. Угол
поворота равен 90 градусов всегда для любого поворота, радиус равен
размеру ячейки деленному на 2. Так как автомобиль движется правее
середины дороги (чтобы обеспечить двухстороннее движение), то радиус
48

ячейки можно получить деля размер клетки не на 2, а умножая на значение


полученное прибавлением к 0.5 длины части ячейки, на которую сдвинут
вправо автомобиль, деленную на длину ячейки. Обновление ориентации и
позиции автомобиля при повороте представлено в листинге 3.5.
Листинг 3.5 – Обновление ориентации и позиции автомобиля при повороте
// здесь: угол между касательной и хордой равен половине дуги
// поворачиваем до хорды, переносим в новом направлении на длину хорды,
// затем снова поворачиваем на тот же угол, чтобы перейти в направление
// новой касательной
pCarMesh->RotateY(rotsgn * fToRotate / 2.0f); // сначала поворачиваем до
хорды (из свойства угол между касательной и хордой равен половине дуги)
pCarMesh->Update(); // обновляем матрицы элемента
pCarMesh->MoveForward(2.0f * (fThisCellSize * devcell) * sinf(fToRotate /
2.0f)); // переносим на длину хорды
pCarMesh->Update();
pCarMesh->RotateY(rotsgn * fToRotate / 2.0f); // поворачиваем до новой
касательной
fAlpha += fToRotate; // считаем общий угол поворота на текущем тайтле
fL += fToMove; // считаем общий пройденный путь по дуге на текущем тайтле

Текущая позиция в ячейке хранится в зависимости от типа движения в


переменных либо fXPos, либо fYPos, либо в fAlpha (угол поворота) и fL
(длина пути, пройденная по дуге при повороте).
В конце метода update определяется пройден ли путь в клетке до конца и
если пройден, происходит переход к следующей клетке.
Для предоставления пользователю возможности управлять самостоятельно
автомобилем в трехмерной сцене разработан класс CPassUserCar, который
является наследником от класса CPassCar. В дочернем классе переопределен
метод update, который определяет следующее направление движения
автомобиля. Для определения пользовательского автомобиля также
необходим файл, описывающий карту пути автомобиля, только в отличие от
предыдущего случая, данная карта хранит только две ключевых точки –
первую и следующую за первой, а также первоначальную позицию
автомобиля в пространстве.
Метод update дочернего класса при движении определяет следующую
ключевую позицию автомобиля в соответствии с картой дорог объекта
класса CPassRoadsCollection и соответствующей нажатой клавишей
пользователя (стрелкой влево, стрелкой вправо, стрелкой вверх).
Третий из разработанных классов и относящийся к движению автомобилей
является класс CPassRandomCar, позволяющий двигаться автомобилю по
случайной траектории в зависимости от карты высот. Данный класс
наследуется от класса CPassUserCar и переопределяет метод update, где
указывает направление движения, выбранное случайным образом (заменяя
тем самым нажатие кнопки на клавиатуре пользователем) и вызывает метод
update родительского класса.
Окончательная реализация движения автомобилей в сцене представлена на
рисунке 3.10.
49

Рисунок 3.10 – Сцена с движущимися по ней автомобилями

После добавления пользовательского автомобиля в сцену возможно


привязать к нему камеру с видом от третьего лица. Для привязки камеры к
объекту достаточно после каждого обновления позиции объекта и его
ориентации вызывать метод update класса CEleky3rdPersCamera (листинг
3.6).

Листинг 3.6 – Привязка камеры к объекту пользователя


pCam3rd->Update(pUserCar->pCarMesh->GetPosition(),
pUserCar->pCarMesh->GetYRotationCos(),
pUserCar >pCarMesh->GetYRotationSin());

Также после появления пользовательского объекта возможно добавить


аркадную составляющую в симулятор, которая позволит пользователям
управляя автомобилем за заданный промежуток времени проехать через все
отметки на дороге, расположенные случайным образом. Работа с данной
составляющей реализуется через класс CPassArcade и класс CPassTitles.
Метод init класса CPassArcade распределяет случайным образом по карте
дорог объекты-метки, и добавляет их в класс CPassTitles. При перемещении
через центр клетки, в которой находится объект, через который необходимо
проехать, он исчезает из карты класса CPassTitles и значение выполненного
задания в классе CPassArcade инкрементируется. Рисунок 3.11
демонстрирует участок сцены законченного приложения.
50

Рисунок 3.11 Участок сцены законченного приложения.

Возможности разработанного приложения более детально


продемонстрированы в приложении Б.
51

ЗАКЛЮЧЕНИЕ
В результате выполненной работы были разработаны библиотеки,
позволяющие на их основе создавать высокопроизводительные современные
приложения для обработки трехмерной графики. Примером таких
приложений могут быть видеоигры. Данные библиотеки содержат решения
проблем обработки трехмерной графики используя интерфейсы DirectX. Все
решения представляют в полном объеме современные технологии
обработки, трансформации, анимации и оптимизации объектов трехмерной
графики, такие как: работа с картами высот, генерирование ландшафтов,
работа со статическими объектами, шейдерами, скелетной анимацией,
виртуальной камерой и другие технологии.
Также в ходе выполнения работы была разработана видеоигра «Симулятор
дорожного движения», которая построена на основе разработанного ядра и
реализует большинство его возможностей. Видеоигра предназначена как для
демонстрации разработанных в библиотеках ядра функций, так и для
использования пользователями в развлекательных целях. Моделирование
объектов для приложения осуществлялось в программе 3ds max, однако
разработанное графическое ядро поддерживает формат объектов .x, который
широко распространен и может быть получен из многих других редакторов.
При разработке особое внимание было уделено возможности разделения
трехмерных пространств на отдельные части и быстрого ориентирования в
них для реализации функций отображения большого числа объектов на
любых расстояниях относительно независимо от возможностей аппаратного
обеспечения.
В основе разработанного приложения лежит большое количество
математических алгоритмов, решений проблем оптимизации и решений
начальных физических задач, что может быть использовано как при
дальнейшем усовершенствовании приложения, так и при разработке других
приложений. При разработке был максимально использован объектно-
ориентированный подход, что позволит эффективно как дополнять
разработанные библиотеки и приложение, так и использовать каждый из
компонентов в других приложениях.
В работе показана эффективность применения интерфейсов библиотеки
DirectX и разработанных на ее основе классов ядра графических программ
для решения сложных задач по обработке трехмерной графики. Результатом
грамотного внедрения библиотеки DirectX и разработанного ядра при
написании графических приложений является эффективное использование
ресурсов компьютера, уменьшение времени, затрачиваемого на разработку
подобных приложений, а также, что немаловажно, следование современным
тенденциям в программировании графических приложений. Разработанное
ядро может применяться при программировании как мультимедийных
приложений, требующих быстрой обработки трехмерной графики, так и
видеоигр различной сложности.
52

СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ


1 Адамс, Д. DirectX: продвинутая анимация. : пер. с англ. / Д.
Адамс. – М. : ИД «КУДИЦ-ОБРАЗ», 2004.
2 Евченко, А. И. OpenGL и DirectX. Программирование графики.
Для профессионалов / А. И. Евченко. – Санкт-Петербург : СПб Питер, 2006.
3 Либерти, Д. Освой самостоятельно С++ за 21 день. : 3-е изд, пер.
с англ. / Д. Либерти. – М. : Издательский дом «Вильямс», 2000.
4 Мэрдок, К. Л. 3ds max 4. Библия пользователя. : пер. с англ. / К.
Л. Мэрдок. – М. : Издательский дом «Вильямс», 2002.
5 Фленов, М. DirectX и C++. Искусство программирования / М.
Фленов. - Санкт-Петербург : BHV-Санкт-Петербург, 2006.
6 Akenine-Moller, T. Real-Time Rendering. Third Edition / Tomas
Akenine-Moller. – Wellesley : A K Peters, Ltd, 2008
7 Jones W. Beginning DirectX 9 / Wendy Jones. – Boston: Course PTR,
2004.
8 LaMothe, A. Beginning Direct3D Game Programming. Second
Edition / Andre LaMothe. – Boston : Premier Press, 2003.
9 Luna, Frank D. Introduction to 3D Game Programming with DirectX
9.0c : A Shader Approach / Frank D. Luna. – Los Rios : Wordware Publishing,
Inc, 2006.
10 St-Laurent, S. Shaders for game programmers and artists / S. St-
Laurent. – Boston : Thomson Course Technology, 2004.
11 Thorn, A. DirectX 9 Graphics: the definitive guide to Direct3D / A.
Thorn. – Los Rios : Worldwide Publishing Inc., 2005.
12 Walsh, P. Advanced 3D game programming with DirectX 9.0 / P.
Walsh. – Los Rios : Worldwide Publishing Inc., 2003.
13 DirectX 9.0 Programmer's Reference. – Microsoft Corporation,
2002.
53

Приложение А

Листинг программы
#include "ElekyTerrain.h"

CElekyTerrain::CElekyTerrain(LPDIRECT3DDEVICE9 pd3dDevice,
int nCols,
int nRows,
int iSpacing,
float fHeightScale) :
terrainFVF(D3DFVF_XYZ | D3DFVF_TEX1)
{
this->pd3dDevice = pd3dDevice;

this->iWidth = nCols * iSpacing;


this->iDepth = nRows * iSpacing;

this->nVertsPerRow = nCols + 1;
this->nVertsPerCol = nRows + 1;

this->nCols = nCols;
this->nRows = nRows;

this->nVertices = nVertsPerRow * nVertsPerCol;


this->iSpacing = iSpacing;

this->nTriangles = nCols * nRows * 2;

this->fHeightScale = fHeightScale;

D3DXMatrixIdentity (&matWorld);
}

CElekyTerrain::~CElekyTerrain(void)
{
}

bool CElekyTerrain::init(char *strFile, char *strTexture)


{
if(strFile)
{
if(!loadHeightMap(strFile))
return false;
}
else
{
vHeightmap.resize(nVertices);
for(int i = 0; i < nVertices; i++)
vHeightmap[i] = 0;
}

if(!computeVertices())
return false;

if(!computeIndices())
return false;
54

if(strTexture)
{
if(!loadTexture(strTexture))
return false;
}

return true;
}

bool CElekyTerrain::loadTexture(char *strFile)


{
HRESULT hr;
hr = D3DXCreateTextureFromFile(pd3dDevice, strFile, &pTexture);

if(FAILED(hr))
return false;

return true;
}

bool CElekyTerrain::loadHeightMap(char *strFile)


{
// Высота для каждой вершины
std::vector<BYTE> in(nVertices);

std::ifstream inFile(strFile, std::ios_base::binary);

if(inFile == 0)
return false;

inFile.read((char*)&in[0], // буффер
in.size()); // количество читаемых в буфер байт

inFile.close();

// копируем вектор BYTE в вектор int


vHeightmap.resize(nVertices);
for(size_t i = 0; i < in.size(); i++)
vHeightmap[i] = in[i];

return true;
}

int CElekyTerrain::getHeightmapEntry(int row, int col)


{
return vHeightmap[((nRows)-row) * nVertsPerRow + col];
}

int CElekyTerrain::getHeightmapForLerp(int row, int col)


{
return vHeightmap[row * nVertsPerRow + col];
}

void CElekyTerrain::setHeightmapEntry(int row, int col, int value)


{
vHeightmap[((nRows)-row) * nVertsPerRow + col] = value;
}

float CElekyTerrain::getHeight(float x, float z)


{
// Выполняем преобразование перемещения для плоскости XZ,
55

// чтобы точка START ландшафта совпала с началом координат.


x = ((float)iWidth / 2.0f) + x;
z = ((float)iDepth / 2.0f) - z;

// Масштабируем сетку таким образом, чтобы размер


// каждой ее ячейки стал равен 1.
x /= (float)iSpacing;
z /= (float)iSpacing;

float col = floorf(x);


float row = floorf(z);

if(col < 0 || row < 0 || col > nCols || row > nRows)
return 0;

// A B
// *---*
// | / |
// *---*
// C D
float A = (float)getHeightmapForLerp((int)row, (int)col);
float B = (float)getHeightmapForLerp((int)row, (int)col+1);
float C = (float)getHeightmapForLerp((int)row+1, (int)col);
float D = (float)getHeightmapForLerp((int)row+1, (int)col+1);

float dx = x - col;
float dz = z - row;

float height;
if(dz < 1.0f - dx) // верхний треугольник ABC
{
float uy = B - A; // A->B
float vy = C - A; // A->C

height = A + Lerp(0.0f, uy, dx) + Lerp(0.0f, vy, dz);


}
else // нижний треугольник DCB
{
float uy = C - D; // D->C
float vy = B - D; // D->B

height = D + Lerp(0.0f, uy, 1.0f - dx) + Lerp(0.0f, vy, 1.0f -


dz);
}

return height;
}

inline float CElekyTerrain::Lerp(float a, float b, float t)


{
return a - (a*t) + (b*t);
}

bool CElekyTerrain::computeVertices()
{
HRESULT hr = 0;

hr = pd3dDevice->CreateVertexBuffer(nVertices * sizeof(TerrainVertex),

D3DUSAGE_WRITEONLY,
terrainFVF,
D3DPOOL_MANAGED,
56

&pVertexBuffer,
0);
if(FAILED(hr))
return false;

int startX = -iWidth / 2;


int startZ = iDepth / 2;
int endX = iWidth / 2;
int endZ = -iDepth / 2;

// вычисляем приращение координат текстуры


// при переходе от одной вершины к другой.
//float uCoordIncrementSize = 1.0f / (float)_numCellsPerRow;
//float vCoordIncrementSize = 1.0f / (float)_numCellsPerCol;

TerrainVertex* v = NULL;
pVertexBuffer->Lock(0, 0, (void**)&v, 0);

int i = 0;
for(int z = startZ; z >= endZ; z -= iSpacing)
{
int j = 0;
for(int x = startX; x <= endX; x += iSpacing)
{
int index = i * nVertsPerRow + j;

v[index].x = (float)x;
v[index].y = (float)vHeightmap[index];
v[index].z = (float)z;

//v[index].rhw = 1.0f;

//v[index].color = 0xff000000 | RGB(rand()%255,rand()


%255,rand()%255);

v[index].u = (float)j * 1,
v[index].v = (float)i * 1,

j++;
}
i++;
}

pVertexBuffer->Unlock();

return true;
}

bool CElekyTerrain::GetRayIntersection(D3DXVECTOR3 *vret, D3DXVECTOR3 *vOrig,


D3DXVECTOR3 *vDir)
{
D3DXVECTOR3 vr;

TerrainVertex* v = NULL;
pVertexBuffer->Lock(0, 0, (void**)&v, 0);

int index = 0;
for(int i = 0; i <= nRows; i++)
{
for(int j = 0; j <= nCols; j++)
{
D3DXVECTOR3 s1, s2, s3;
57

s1.x = v[i * nVertsPerRow + j].x;


s1.y = v[i * nVertsPerRow + j].y;
s1.z = v[i * nVertsPerRow + j].z;

s2.x = v[i * nVertsPerRow + j + 1].x;


s2.y = v[i * nVertsPerRow + j + 1].y;
s2.z = v[i * nVertsPerRow + j + 1].z;

s3.x = v[(i+1) * nVertsPerRow + j].x;


s3.y = v[(i+1) * nVertsPerRow + j].y;
s3.z = v[(i+1) * nVertsPerRow + j].z;

if(CElekyMath::GetCrossPoint(*vOrig, *vDir, s1, s2, s3, vr))


{
if(CElekyMath::IsPointInTriangle(vr, s1, s2, s3))
{
pVertexBuffer->Unlock();
*vret = vr;
return true;
}
}

s1.x = v[(i+1) * nVertsPerRow + j].x;


s1.y = v[(i+1) * nVertsPerRow + j].y;
s1.z = v[(i+1) * nVertsPerRow + j].z;

s2.x = v[i * nVertsPerRow + j + 1].x;


s2.y = v[i * nVertsPerRow + j + 1].y;
s2.z = v[i * nVertsPerRow + j + 1].z;

s3.x = v[(i+1) * nVertsPerRow + j + 1].x;


s3.y = v[(i+1) * nVertsPerRow + j + 1].y;
s3.z = v[(i+1) * nVertsPerRow + j + 1].z;

if(CElekyMath::GetCrossPoint(*vOrig, *vDir, s1, s2, s3, vr))


{
if(CElekyMath::IsPointInTriangle(vr, s1, s2, s3))
{
pVertexBuffer->Unlock();
*vret = vr;
return true;
}
}
}
}

pVertexBuffer->Unlock();

return false;
}

bool CElekyTerrain::computeIndices()
{
HRESULT hr = 0;

hr = pd3dDevice->CreateIndexBuffer(nTriangles * 3 * sizeof(WORD), // 3
индекса на треугольник
D3DUSAGE_WRITEONLY,
D3DFMT_INDEX16,
D3DPOOL_MANAGED,
&pIndexBuffer,
0);
58

if(FAILED(hr))
return false;

WORD* pIndices = NULL;


pIndexBuffer->Lock(0, 0, (void**)&pIndices, 0);

// Индекс, с которого начинается группа из 6 индексов,


// описывающая два треугольника, образующих квадрат
int baseIndex = 0;

for(int i = 0; i < nRows; i++)


{
for(int j = 0; j < nCols; j++)
{
pIndices[baseIndex] = i * nVertsPerRow + j;
pIndices[baseIndex + 1] = i * nVertsPerRow + j + 1;
pIndices[baseIndex + 2] = (i+1) * nVertsPerRow + j;
pIndices[baseIndex + 3] = (i+1) * nVertsPerRow + j;
pIndices[baseIndex + 4] = i * nVertsPerRow + j + 1;
pIndices[baseIndex + 5] = (i+1) * nVertsPerRow + j + 1;

baseIndex += 6;
}
}

pIndexBuffer->Unlock();

return true;
}

void CElekyTerrain::render()
{
pd3dDevice->SetTransform( D3DTS_WORLD, &matWorld );

pd3dDevice->SetStreamSource( 0, pVertexBuffer, 0,
sizeof(TerrainVertex) );
pd3dDevice->SetFVF( terrainFVF );
pd3dDevice->SetIndices( pIndexBuffer );
pd3dDevice->SetTexture( 0, pTexture );

pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST,
0,
0,
nVertices,
0,
nTriangles );

#include "ElekyCulling.h"

CElekyCulling::CElekyCulling(LPDIRECT3DDEVICE9 pd3dDevice, float fLength,


float fCellSize)
{
this->pd3dDevice = pd3dDevice;
this->fCellSize = fCellSize;

miny = 0;
maxy = 255;

pTopNode = new QUADNODE();


makeTree(pTopNode, -fLength / 2.0f, -fLength / 2.0f, fLength);

nCells = (int)(fLength / fNodeSize);


59

pNodes = new QUADNODE*[nCells*nCells];


fillTree();
}

CElekyCulling::~CElekyCulling(void)
{
deleteTree(pTopNode);
delete [] pNodes;
}

void CElekyCulling::init(D3DXMATRIX &matView, D3DXMATRIX &matProj)


{
D3DXMATRIX matViewProj;
/* D3DXMATRIX matView, matProj;
pd3dDevice->GetTransform(D3DTS_VIEW, &matView);
pd3dDevice->GetTransform(D3DTS_PROJECTION, &matProj);
*/
matViewProj = matView * matProj;

vFrustum[0].a = matViewProj._14 + matViewProj._11;


vFrustum[0].b = matViewProj._24 + matViewProj._21;
vFrustum[0].c = matViewProj._34 + matViewProj._31;
vFrustum[0].d = matViewProj._44 + matViewProj._41;

// Right plane
vFrustum[1].a = matViewProj._14 - matViewProj._11;
vFrustum[1].b = matViewProj._24 - matViewProj._21;
vFrustum[1].c = matViewProj._34 - matViewProj._31;
vFrustum[1].d = matViewProj._44 - matViewProj._41;

// Top plane
vFrustum[2].a = matViewProj._14 - matViewProj._12;
vFrustum[2].b = matViewProj._24 - matViewProj._22;
vFrustum[2].c = matViewProj._34 - matViewProj._32;
vFrustum[2].d = matViewProj._44 - matViewProj._42;

// Bottom plane
vFrustum[3].a = matViewProj._14 + matViewProj._12;
vFrustum[3].b = matViewProj._24 + matViewProj._22;
vFrustum[3].c = matViewProj._34 + matViewProj._32;
vFrustum[3].d = matViewProj._44 + matViewProj._42;

// Near plane
vFrustum[4].a = matViewProj._13;
vFrustum[4].b = matViewProj._23;
vFrustum[4].c = matViewProj._33;
vFrustum[4].d = matViewProj._43;

// Far plane
vFrustum[5].a = matViewProj._14 - matViewProj._13;
vFrustum[5].b = matViewProj._24 - matViewProj._23;
vFrustum[5].c = matViewProj._34 - matViewProj._33;
vFrustum[5].d = matViewProj._44 - matViewProj._43;
}

void CElekyCulling::makeTree(QUADNODE *pNode, float fx, float fz, float


fThisCellSize)
{
pNode->minx = fx;
pNode->minz = fz;
pNode->maxx = fx + fThisCellSize;
pNode->maxz = fz + fThisCellSize;
60

if(fThisCellSize <= ( fCellSize * 2.0f ))


{
// останавливаемся в делении
fNodeSize = fThisCellSize;

pNode->pBranches[0] = NULL;
pNode->pBranches[1] = NULL;
pNode->pBranches[2] = NULL;
pNode->pBranches[3] = NULL;

pNode->pObjects = new std::vector<void *>;


return;
}

pNode->pBranches[0] = new QUADNODE();


pNode->pBranches[1] = new QUADNODE();
pNode->pBranches[2] = new QUADNODE();
pNode->pBranches[3] = new QUADNODE();

this->makeTree(pNode->pBranches[0], fx, fz, fThisCellSize / 2.0f);


this->makeTree(pNode->pBranches[1], fx + fThisCellSize / 2.0f, fz,
fThisCellSize / 2.0f);
this->makeTree(pNode->pBranches[2], fx, fz + fThisCellSize / 2.0f,
fThisCellSize / 2.0f);
this->makeTree(pNode->pBranches[3], fx + fThisCellSize / 2.0f, fz +
fThisCellSize / 2.0f, fThisCellSize / 2.0f);
}

void CElekyCulling::fillTree()
{
for(int i = -nCells / 2; i < nCells / 2; i++)
{
for(int j = -nCells / 2; j < nCells / 2; j++)
{
int index = (i + nCells / 2) * nCells + (j + nCells / 2);
pNodes[index] = getPointer(i * fNodeSize + fNodeSize / 2.0f,
j * fNodeSize +
fNodeSize / 2.0f,
pTopNode);
if(!pNodes[index])
int t = 2;

}
}
}

QUADNODE *CElekyCulling::getPointer(float fx, float fz, QUADNODE *pNode)


{
if(pNode->minx < fx && pNode->minz < fz && pNode->maxx > fx && pNode-
>maxz > fz)
{
if(pNode->pBranches[0])
{
QUADNODE *t;
for(int i = 0; i < 4; i++)
{
t = getPointer(fx, fz, pNode->pBranches[i]);
if(t) return t;
}
}
else
61

return pNode;
}
return NULL;
}

void CElekyCulling::deleteTree(QUADNODE *pNode)


{
if(pNode->pBranches[0])
{
deleteTree(pNode->pBranches[0]);
deleteTree(pNode->pBranches[1]);
deleteTree(pNode->pBranches[2]);
deleteTree(pNode->pBranches[3]);
}
else
{
pNode->pObjects->clear();
delete pNode->pObjects;
}
delete pNode;
}

bool CElekyCulling::in(QUADNODE *pNode)


{
float minx = pNode->minx;
float minz = pNode->minz;
float maxx = pNode->maxx;
float maxz = pNode->maxz;

// проходим по каждой из 6 плоскостей камеры


// Если оказалось, что перед каждой плоскостью пирамиды находится хотя
бы по одной вершине куба,
// значит, он видим.
D3DXVECTOR3 vOut;
for (int i = 0; i < 6; i++)
{
if (D3DXPlaneDotCoord(&(vFrustum[i]), &D3DXVECTOR3(minx, miny,
minz)) > 0) continue;
if (D3DXPlaneDotCoord(&(vFrustum[i]), &D3DXVECTOR3(minx, miny,
maxz)) > 0) continue;
if (D3DXPlaneDotCoord(&(vFrustum[i]), &D3DXVECTOR3(maxx, miny,
minz)) > 0) continue;
if (D3DXPlaneDotCoord(&(vFrustum[i]), &D3DXVECTOR3(maxx, miny,
maxz)) > 0) continue;
if (D3DXPlaneDotCoord(&(vFrustum[i]), &D3DXVECTOR3(minx, maxy,
minz)) > 0) continue;
if (D3DXPlaneDotCoord(&(vFrustum[i]), &D3DXVECTOR3(minx, maxy,
maxz)) > 0) continue;
if (D3DXPlaneDotCoord(&(vFrustum[i]), &D3DXVECTOR3(maxx, maxy,
minz)) > 0) continue;
if (D3DXPlaneDotCoord(&(vFrustum[i]), &D3DXVECTOR3(maxx, maxy,
maxz)) > 0) continue;

return false;
}
return true;
}

void CElekyCulling::passTreeToRender(QUADNODE *pNode)


{
if(pNode->pBranches[0])
{
62

if(in(pNode->pBranches[0]))
passTreeToRender(pNode->pBranches[0]);
if(in(pNode->pBranches[1]))
passTreeToRender(pNode->pBranches[1]);
if(in(pNode->pBranches[2]))
passTreeToRender(pNode->pBranches[2]);
if(in(pNode->pBranches[3]))
passTreeToRender(pNode->pBranches[3]);
}
else
{
for(size_t i = 0; i < pNode->pObjects->size(); i++)
{
CElekyMesh *pMesh = (CElekyMesh *)pNode->pObjects->at(i);
if(!pMesh->rendered())
pMesh->render();
}
}
}

void CElekyCulling::passTreeAfterRender(QUADNODE *pNode)


{
if(pNode->pBranches[0])
{
if(in(pNode->pBranches[0]))
passTreeAfterRender(pNode->pBranches[0]);
if(in(pNode->pBranches[1]))
passTreeAfterRender(pNode->pBranches[1]);
if(in(pNode->pBranches[2]))
passTreeAfterRender(pNode->pBranches[2]);
if(in(pNode->pBranches[3]))
passTreeAfterRender(pNode->pBranches[3]);
}
else
{
for(size_t i = 0; i < pNode->pObjects->size(); i++)
{
CElekyMesh *pMesh = (CElekyMesh *)pNode->pObjects->at(i);
pMesh->resetRender();
}
}
}

void CElekyCulling::render()
{
passTreeToRender(pTopNode);
passTreeAfterRender(pTopNode);
}

void CElekyCulling::put(D3DXVECTOR3 vMin, D3DXVECTOR3 vMax, void *pObject)


{
for(float i = (vMin.x / fNodeSize); i <= (vMax.x / fNodeSize);
i+=fNodeSize)
{
for(float j = (vMin.z / fNodeSize); j <= (vMax.z / fNodeSize);
j+=fNodeSize)
{
int index = ((int)i + nCells / 2) * nCells + ((int)j +
nCells / 2);

QUADNODE *pN = pNodes[index];


pN->pObjects->push_back(pObject);
63

}
}
}

void CElekyCulling::remove(D3DXVECTOR3 vMin, D3DXVECTOR3 vMax, void *pObject)


{
for(float i = (vMin.x / fNodeSize); i <= (vMax.x / fNodeSize);
i+=fNodeSize)
{
for(float j = (vMin.z / fNodeSize); j <= (vMax.z / fNodeSize);
j+=fNodeSize)
{
int index = ((int)i + nCells / 2) * nCells + ((int)j +
nCells / 2);
QUADNODE *pN = pNodes[index];
std::vector<void *>::iterator toDel;
for(std::vector<void *>::iterator q = pN->pObjects->begin();
q != pN->pObjects->end(); q++)
{
if(*q == pObject)
toDel = q;
}
pN->pObjects->erase(toDel);
}
}
}
#include "ElekySprite.h"

CElekySprite::CElekySprite(LPDIRECT3DDEVICE9 pd3dDevice)
{
this->pd3dDevice = pd3dDevice;
pSprite = NULL;
}

CElekySprite::~CElekySprite(void)
{
if(pSprite)
pSprite->Release();
}

bool CElekySprite::init(char *strFile, DWORD dwTranspColor)


{
HRESULT hr;

hr = D3DXCreateSprite(pd3dDevice, &pSprite);
if(FAILED(hr))
return false;

hr = D3DXCreateTextureFromFileEx(pd3dDevice, strFile,
D3DX_DEFAULT,
D3DX_DEFAULT,
D3DX_DEFAULT,
NULL,
D3DFMT_UNKNOWN,
D3DPOOL_MANAGED,
D3DX_FILTER_NONE,
//D3DX_DEFAULT,
D3DX_DEFAULT,
dwTranspColor,
NULL,
NULL,
&pTexture);
64

if(FAILED(hr))
return false;

return true;
}

void CElekySprite::render(float x, float y, RECT *pSrcRect)


{
pSprite->Begin();

pSprite->Draw(pTexture, pSrcRect, NULL, NULL, 0, &D3DXVECTOR2(x, y),


0xffffffff);

pSprite->End();
}

void CElekySprite::render(float x, float y, float rotcx, float rotcy, float


r, RECT *pSrcRect)
{
pSprite->Begin();

pSprite->Draw(pTexture, pSrcRect, NULL, &D3DXVECTOR2(rotcx, rotcy), r,


&D3DXVECTOR2(x, y), 0xffffffff);

pSprite->End();
}

void CElekySprite::render(RECT *pDestRect, RECT *pSrcRect)


{
pSprite->Begin();

pSprite->Draw(pTexture, pSrcRect, NULL, NULL, 0,


&D3DXVECTOR2((float)pDestRect->left, (float)pDestRect->top), 0xffffffff);

pSprite->End();
}

#include "PassRoad.h"

CPassRoad::CPassRoad(LPDIRECT3DDEVICE9 pd3dDevice, CElekyTerrain *pTerrain,


CPassRoadCollection *pRoadCollection)
{
this->pd3dDevice = pd3dDevice;
this->pTerrain = pTerrain;
this->pRoadCollection = pRoadCollection;

pStraightMesh = NULL;
pRoundMesh = NULL;
pTMesh = NULL;
pXMesh = NULL;
vRoads = NULL;
pMatWorld = NULL;
iTotalRoads = 0;

fCellSize = (float)pTerrain->iSpacing;
}

CPassRoad::~CPassRoad(void)
{
if(pStraightMesh)
65

delete pStraightMesh;

if(pRoundMesh)
delete pRoundMesh;

if(pTMesh)
delete pTMesh;

if(pXMesh)
delete pXMesh;

if(vRoads)
delete [] vRoads;

if(pMatWorld)
delete [] pMatWorld;
}

void CPassRoad::init(char *strDescFile, char *strStraightMesh, char


*strRoundMesh,
char *strTMesh, char *strXMesh)
{
FILE *file = fopen(strDescFile, "rb");
fscanf(file, "%d ", &iTotalRoads);

vRoads = new VROAD[iTotalRoads];


for(int i = 0; i < iTotalRoads; i++)
fscanf(file, "%d %d %d ", &(vRoads[i].x), &(vRoads[i].y),
&(vRoads[i].type));

fclose(file);

pStraightMesh = new CElekyMesh(pd3dDevice);


pStraightMesh->init(strStraightMesh, "");

pRoundMesh = new CElekyMesh(pd3dDevice);


pRoundMesh->init(strRoundMesh, "");

pTMesh = new CElekyMesh(pd3dDevice);


pTMesh->init(strTMesh, "");

pXMesh = new CElekyMesh(pd3dDevice);


pXMesh->init(strXMesh, "");

pMatWorld = new D3DXMATRIX[iTotalRoads];


for(int i = 0; i < iTotalRoads; i++)
{
pRoadCollection->SetRoadType(vRoads[i].y, vRoads[i].x,
vRoads[i].type);
}

// для того чтобы выровнять ландшафт по дороге


// сортируем по убыванию высоты тайтлы
// чтобы установить сначала те, которые выше
bool t = true;
while(t)
{
t = false;
for(int i = 0; i < (iTotalRoads - 1); i++)
{
if(getMaxTerHeight(i) < getMaxTerHeight(i+1))
{
66

VROAD v = vRoads[i];
vRoads[i] = vRoads[i+1];
vRoads[i+1] = v;
t = true;
}
}
}

// обновляем участки которые требуют прямого ландшафта


for(int i = 0; i < iTotalRoads; i++)
{
if(vRoads[i].type > 2)
update(i);
}

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


for(int i = 0; i < iTotalRoads; i++)
{
if(vRoads[i].type <= 2)
update(i);
}
}

float CPassRoad::getMaxTerHeight(int i)
{
float h = (float)pTerrain->getHeightmapEntry(vRoads[i].y, vRoads[i].x);
if(h < (float)pTerrain->getHeightmapEntry(vRoads[i].y + 1, vRoads[i].x))
h = (float)pTerrain->getHeightmapEntry(vRoads[i].y + 1,
vRoads[i].x);
if(h < (float)pTerrain->getHeightmapEntry(vRoads[i].y, vRoads[i].x + 1))
h = (float)pTerrain->getHeightmapEntry(vRoads[i].y, vRoads[i].x +
1);
if(h < (float)pTerrain->getHeightmapEntry(vRoads[i].y + 1, vRoads[i].x +
1))
h = (float)pTerrain->getHeightmapEntry(vRoads[i].y + 1,
vRoads[i].x + 1);

return h;
}

void CPassRoad::update(int i)
{
float h, h2;

if(vRoads[i].type == 1)
{
h = pTerrain->getHeightmapEntry(vRoads[i].y, vRoads[i].x) >
pTerrain->getHeightmapEntry(vRoads[i].y, vRoads[i].x+1) ?
(float)pTerrain->getHeightmapEntry(vRoads[i].y, vRoads[i].x)
:
(float)pTerrain->getHeightmapEntry(vRoads[i].y,
vRoads[i].x+1);
h2 = pTerrain->getHeightmapEntry(vRoads[i].y + 1, vRoads[i].x) >
pTerrain->getHeightmapEntry(vRoads[i].y + 1, vRoads[i].x +
1) ?
(float)pTerrain->getHeightmapEntry(vRoads[i].y + 1,
vRoads[i].x) :
(float)pTerrain->getHeightmapEntry(vRoads[i].y + 1,
vRoads[i].x + 1);
}
else if(vRoads[i].type == 2)
{
67

h = pTerrain->getHeightmapEntry(vRoads[i].y, vRoads[i].x) >


pTerrain->getHeightmapEntry(vRoads[i].y + 1, vRoads[i].x) ?
(float)pTerrain->getHeightmapEntry(vRoads[i].y, vRoads[i].x)
:
(float)pTerrain->getHeightmapEntry(vRoads[i].y + 1,
vRoads[i].x);
h2 = pTerrain->getHeightmapEntry(vRoads[i].y, vRoads[i].x + 1) >
pTerrain->getHeightmapEntry(vRoads[i].y + 1, vRoads[i].x +
1) ?
(float)pTerrain->getHeightmapEntry(vRoads[i].y, vRoads[i].x
+ 1) :
(float)pTerrain->getHeightmapEntry(vRoads[i].y + 1,
vRoads[i].x + 1);
}
else // T and X crosses
{
// выровнять ландшафт
h = getMaxTerHeight(i);

pTerrain->setHeightmapEntry(vRoads[i].y, vRoads[i].x, (int)h);


pTerrain->setHeightmapEntry(vRoads[i].y + 1, vRoads[i].x, (int)h);
pTerrain->setHeightmapEntry(vRoads[i].y, vRoads[i].x + 1, (int)h);
pTerrain->setHeightmapEntry(vRoads[i].y + 1, vRoads[i].x + 1,
(int)h);
h2 = h;
}

float r = fabs(h2-h);
float sgn = h2 > h ? -1.0f : 1.0f;
float a = (float)atan( r / fCellSize);
pStraightMesh->IdentityMatrices();
pStraightMesh->SetScale(1.0f, 1.0f,
sqrtf(fCellSize * fCellSize + r * r) / fCellSize);
pStraightMesh->RotateX(sgn*a);
if(vRoads[i].type == 2)
pStraightMesh->RotateY( D3DX_PI / 2.0f );

if(vRoads[i].type == 3 || vRoads[i].type == 15)


pStraightMesh->RotateY( D3DX_PI / 2.0f );
if(vRoads[i].type == 4 || vRoads[i].type == 14)
pStraightMesh->RotateY( - D3DX_PI / 2.0f );
if(vRoads[i].type == 5 || vRoads[i].type == 13)
pStraightMesh->RotateY( D3DX_PI );

pStraightMesh->SetPosition((vRoads[i].x - ((pTerrain->nCols - 1) /
2.0f)) * fCellSize,
((h+h2)/2.0f),
(vRoads[i].y - ((pTerrain->nRows - 1) / 2.0f)) * fCellSize);
pStraightMesh->Update();
pMatWorld[i] = pStraightMesh->GetMatWorld();
}

void CPassRoad::render()
{
for(int i = 0; i < iTotalRoads; i++)
{
if(vRoads[i].type == 1 || vRoads[i].type == 2 ||
vRoads[i].type == 11 || vRoads[i].type == 12)
{
pStraightMesh->SetMatWorld(pMatWorld[i]);
pStraightMesh->render();
}
68

else if(vRoads[i].type >= 3 && vRoads[i].type <= 10)


{
pRoundMesh->SetMatWorld(pMatWorld[i]);
pRoundMesh->render();
}
else if(vRoads[i].type >= 13 && vRoads[i].type <= 16)
{
pTMesh->SetMatWorld(pMatWorld[i]);
pTMesh->render();
}
else if(vRoads[i].type == 17)
{
pXMesh->SetMatWorld(pMatWorld[i]);
pXMesh->render();
}
}
}

#include "PassCar.h"

void CPassCar::init(char *strObjectFile, char *strPosFile)


{
FILE *file = fopen(strPosFile, "rb");
fscanf(file, "%d ", &iTotalPoints);

iPoints = new VPOINTS[iTotalPoints];


for(int i = 0; i < iTotalPoints; i++)
fscanf(file, "%d %d ", &(iPoints[i].x), &(iPoints[i].y));

fscanf(file, "%f %f %f %d", &(vInitPos.x), &(vInitPos.y),


&(vInitPos.z), &iDirOfCar);
fclose(file);

pCarMesh = new CElekyMesh(pd3dDevice);


pCarMesh->init(strObjectFile, "models/");
pCarMesh->SetPosition(vInitPos.x,
pTerrain->getHeight(vInitPos.x, vInitPos.z) + vInitPos.y, vInitPos.z);

if(iDirOfCar == 2)
pCarMesh->RotateY(D3DX_PI / 2);
else if(iDirOfCar == 3)
pCarMesh->RotateY(D3DX_PI);
else if(iDirOfCar == 4)
pCarMesh->RotateY(-D3DX_PI / 2);

pCarMesh->Update();
pCarMesh->Strife(fStrife);
pCarMesh->Update();

pTitles->OccupyCell(iPoints[0].y, iPoints[0].x, id, iDirOfCar);


//fSpeed = 2.0f;
iCurPoint = 0;
}

bool CPassCar::stopIfNeed()
{
// проверяем следующую ячейку
int i = (iCurPoint >= (iTotalPoints - 1)) ? 0 : iCurPoint + 1;

// ячейка свободна
if(pTitles->GetIsFree(iPoints[i].y, iPoints[i].x, iDirOfCar) == (-1))
{
69

// занимаем ячейку
pTitles->SetIsFree(iPoints[i].y, iPoints[i].x, id, iDirOfCar);
return false;
}
// ячейка занята мной
else if(pTitles->GetIsFree(iPoints[i].y, iPoints[i].x, iDirOfCar) == id)
return false;

// ячейка занята
return true;
}

bool CPassCar::slowIfNeed()
{
// проверяем следующую ячейку
int i = (iCurPoint >= (iTotalPoints - 1)) ? 0 : iCurPoint + 1;

if(pTitles->GetIsFree(iPoints[i].y, iPoints[i].x, iDirOfCar) == (-1))


{
// занимаем ячейку
pTitles->OccupyCell(iPoints[i].y, iPoints[i].x, id, iDirOfCar);
return false;
}
// ячейка занята мной
else if(pTitles->GetIsFree(iPoints[i].y, iPoints[i].x, iDirOfCar) == id)
return false;

return true;
}

void CPassCar::update()
{
// следующая точка
int iNextPoint = iCurPoint >= (iTotalPoints - 1) ? 0 : iCurPoint + 1;

// если дошли до конца траекории то начать сначала или закончить


движение
if(iCurPoint >= (iTotalPoints))
{
iCurPoint = 0;
//return;
}

// определение высоты объекта с поворотом по оси X


float h, h2;
// выбор высоты из соседних точек тайтла (максимальная высота из двух
соседних точек)
if(iDirOfCar == 1 || iDirOfCar == 21 || iDirOfCar == 41)
{
// A--B выбираем максимальную высоту из A и B
// | | и максимальную высоту из C и D
// C--D
h = pTerrain->getHeightmapEntry(iPoints[iCurPoint].y,
iPoints[iCurPoint].x) >
pTerrain->getHeightmapEntry(iPoints[iCurPoint].y,
iPoints[iCurPoint].x + 1) ?
(float) pTerrain->getHeightmapEntry(iPoints[iCurPoint].y,
iPoints[iCurPoint].x) :
(float) pTerrain->getHeightmapEntry(iPoints[iCurPoint].y,
iPoints[iCurPoint].x + 1);
h2 = pTerrain->getHeightmapEntry(iPoints[iNextPoint].y,
iPoints[iNextPoint].x) >
70

pTerrain->getHeightmapEntry(iPoints[iNextPoint].y,
iPoints[iNextPoint].x + 1) ?
(float) pTerrain->getHeightmapEntry(iPoints[iNextPoint].y,
iPoints[iNextPoint].x) :
(float) pTerrain->getHeightmapEntry(iPoints[iNextPoint].y,
iPoints[iNextPoint].x + 1);
}
else if(iDirOfCar == 3 || iDirOfCar == 23 || iDirOfCar == 43)
{
h = pTerrain->getHeightmapEntry(iPoints[iCurPoint].y + 1,
iPoints[iCurPoint].x) >
pTerrain->getHeightmapEntry(iPoints[iCurPoint].y + 1,
iPoints[iCurPoint].x + 1) ?
(float) pTerrain->getHeightmapEntry(iPoints[iCurPoint].y + 1,
iPoints[iCurPoint].x) :
(float) pTerrain->getHeightmapEntry(iPoints[iCurPoint].y + 1,
iPoints[iCurPoint].x + 1);
h2 = pTerrain->getHeightmapEntry(iPoints[iNextPoint].y + 1,
iPoints[iNextPoint].x) >
pTerrain->getHeightmapEntry(iPoints[iNextPoint].y + 1,
iPoints[iNextPoint].x + 1) ?
(float) pTerrain->getHeightmapEntry(iPoints[iNextPoint].y +
1, iPoints[iNextPoint].x) :
(float) pTerrain->getHeightmapEntry(iPoints[iNextPoint].y +
1, iPoints[iNextPoint].x + 1);
}
else if(iDirOfCar == 2 || iDirOfCar == 12 || iDirOfCar == 32)
{
h = pTerrain->getHeightmapEntry(iPoints[iCurPoint].y,
iPoints[iCurPoint].x) >
pTerrain->getHeightmapEntry(iPoints[iCurPoint].y + 1,
iPoints[iCurPoint].x) ?
(float) pTerrain->getHeightmapEntry(iPoints[iCurPoint].y,
iPoints[iCurPoint].x) :
(float) pTerrain->getHeightmapEntry(iPoints[iCurPoint].y + 1,
iPoints[iCurPoint].x);
h2 = pTerrain->getHeightmapEntry(iPoints[iNextPoint].y,
iPoints[iNextPoint].x) >
pTerrain->getHeightmapEntry(iPoints[iNextPoint].y + 1,
iPoints[iNextPoint].x) ?
(float) pTerrain->getHeightmapEntry(iPoints[iNextPoint].y,
iPoints[iNextPoint].x) :
(float) pTerrain->getHeightmapEntry(iPoints[iNextPoint].y +
1, iPoints[iNextPoint].x);
}
else if(iDirOfCar == 4 || iDirOfCar == 14 || iDirOfCar == 34)
{
h = pTerrain->getHeightmapEntry(iPoints[iCurPoint].y,
iPoints[iCurPoint].x + 1) >
pTerrain->getHeightmapEntry(iPoints[iCurPoint].y + 1,
iPoints[iCurPoint].x + 1) ?
(float) pTerrain->getHeightmapEntry(iPoints[iCurPoint].y,
iPoints[iCurPoint].x + 1) :
(float) pTerrain->getHeightmapEntry(iPoints[iCurPoint].y + 1,
iPoints[iCurPoint].x + 1);
h2 = pTerrain->getHeightmapEntry(iPoints[iNextPoint].y,
iPoints[iNextPoint].x + 1) >
pTerrain->getHeightmapEntry(iPoints[iNextPoint].y + 1,
iPoints[iNextPoint].x + 1) ?
(float) pTerrain->getHeightmapEntry(iPoints[iNextPoint].y,
iPoints[iNextPoint].x + 1) :
71

(float) pTerrain->getHeightmapEntry(iPoints[iNextPoint].y +
1, iPoints[iNextPoint].x + 1);
}
float r = fabs(h2-h); // средняя высота
float sgn = h2 > h ? -1.0f : 1.0f; // знак поворота
float a = (float)atan( r / fCellSize ); // угол поворота
pCarMesh->RotateXFromZero( sgn * a ); // непосредственное вращение
if(fabs(h2 - h) < 0.0001f)
pCarMesh->SetHeight(h);
float fThisCellSize = sqrtf(fCellSize*fCellSize + r*r);

// переход к следующему направлению движения (если необходимо)


if(iDirOfCar == 1 && (iPoints[iCurPoint].x - iPoints[iNextPoint].x) < 0)
iDirOfCar = 12;
else if(iDirOfCar == 1 &&
(iPoints[iCurPoint].x - iPoints[iNextPoint].x) > 0)
iDirOfCar = 14;
else if(iDirOfCar == 3 &&
(iPoints[iCurPoint].x - iPoints[iNextPoint].x) < 0)
iDirOfCar = 32;
else if(iDirOfCar == 3 &&
(iPoints[iCurPoint].x - iPoints[iNextPoint].x) > 0)
iDirOfCar = 34;
else if(iDirOfCar == 2 &&
(iPoints[iCurPoint].y - iPoints[iNextPoint].y) < 0)
iDirOfCar = 21;
else if(iDirOfCar == 2 &&
(iPoints[iCurPoint].y - iPoints[iNextPoint].y) > 0)
iDirOfCar = 23;
else if(iDirOfCar == 4 &&
(iPoints[iCurPoint].y - iPoints[iNextPoint].y) < 0)
iDirOfCar = 41;
else if(iDirOfCar == 4 &&
(iPoints[iCurPoint].y - iPoints[iNextPoint].y) > 0)
iDirOfCar = 43;

// ускорение
if(bExtremeSlow)
fAccelerate = 0;
else if(fFixedAccel >= 0.001f)
fAccelerate = fFixedAccel;

fSpeed = fSpeed + fExtreme_a * pTiming->rate();


fSpeed = fSpeed + fAccelerate * pTiming->rate();

if(fSpeed > 17.0f)


fSpeed = 17.0f;
else if(fSpeed <= 0.0f)
{
fSpeed = 0.0f;
fExtreme_a = 0.0f;
}

float fToMove = pTiming->rate() * fSpeed; // сколько необходимо пройти


if((iDirOfCar >= 1 && iDirOfCar <=4) && slowIfNeed())
{
if(!bExtremeSlow)
{
bExtremeSlow = true;
fExtreme_a = - (fSpeed * fSpeed) / (2 * (fThisCellSize - 1.5f));
fAccelerate = 0;
}
72

}
else if((iDirOfCar >= 1 && iDirOfCar <=4) && !slowIfNeed())
{
bExtremeSlow = false;
fExtreme_a = 0;
}

if((iDirOfCar == 1 || iDirOfCar == 3) &&


(fYPos + fToMove) > fThisCellSize)
fToMove = fThisCellSize - fYPos;
else if((iDirOfCar == 2 || iDirOfCar == 4) &&
(fXPos + fToMove) > fThisCellSize)
fToMove = fThisCellSize - fXPos;

// если прямолинейное движение, двигаемся


if(iDirOfCar == 1 || iDirOfCar == 2 || iDirOfCar == 3 || iDirOfCar == 4)
pCarMesh->MoveForward(fToMove);

if(iDirOfCar == 1 || iDirOfCar == 3)
fYPos += fToMove;
else if(iDirOfCar == 2 || iDirOfCar == 4)
fXPos += fToMove;
else // поворот
{
float fToRotate; // угол поворота за данный тик
float rotsgn; // знак поворота (по часовой или против)
float devcell; // в какой части тайтла необходимо
if(iDirOfCar == 12 || iDirOfCar == 23 ||
iDirOfCar == 34 || iDirOfCar == 41)
{
rotsgn = 1.0f;
devcell = 0.5f - fStrife / fCellSize;
}
Else
{
rotsgn = -1.0f;
devcell = 0.5f + fStrife / fCellSize;
}
float fArcLen = (fThisCellSize * devcell) * (D3DX_PI / 2.0f);

if(slowIfNeed())
{
if(!bExtremeSlow)
{
bExtremeSlow = true;
fExtreme_a = - (fSpeed * fSpeed) / (2 * (fArcLen - 1.5f));
fAccelerate = 0;
}
}
else
{
bExtremeSlow = false;
fExtreme_a = 0;
}
if((fL + fToMove) > fArcLen)
fToMove = fArcLen - fL;

fToRotate = fToMove / (fThisCellSize * devcell);


if((fAlpha + fToRotate) > (D3DX_PI / 2.0f))
fToRotate = D3DX_PI / 2.0f - fAlpha;
if((fL + fToMove) >= fArcLen)
fToRotate = D3DX_PI / 2.0f - fAlpha;
73

pCarMesh->RotateY(rotsgn * fToRotate / 2.0f);

pCarMesh->Update();
pCarMesh->MoveForward(2.0f * (fThisCellSize * devcell) *
sinf(fToRotate / 2.0f));
pCarMesh->Update();
pCarMesh->RotateY(rotsgn * fToRotate / 2.0f);
fAlpha += fToRotate;
fL += fToMove;

pCarMesh->Update();
// проверка нужно ли перейти к следующему тайтлу (направлению)
if((iDirOfCar == 1 || iDirOfCar == 3)
&& fYPos >= (fThisCellSize - 0.00001f))
{
fYPos = 0;
pTitles->ReleaseCell(iPoints[iCurPoint].y, iPoints[iCurPoint].x, iDirOfCar);
// при въезде в следующую освобождает текущую
iCurPoint++;
}
else if((iDirOfCar == 2 || iDirOfCar == 4) &&
fXPos >= (fThisCellSize - 0.00001f))
{
fXPos = 0;
pTitles->ReleaseCell(iPoints[iCurPoint].y, iPoints[iCurPoint].x, iDirOfCar);
// при въезде в следующую освобождает текущую
iCurPoint++;
}
else if(fAlpha >= (D3DX_PI / 2.0f - 0.001f)
{
fAlpha = 0.0f;
fL = 0.0f;
pTitles->ReleaseCell(iPoints[iCurPoint].y, iPoints[iCurPoint].x, iDirOfCar);
// при въезде в следующую освобождает текущую
iCurPoint++;

if(iDirOfCar == 12 || iDirOfCar == 32)


iDirOfCar = 2;
else if(iDirOfCar == 14 || iDirOfCar == 34)
iDirOfCar = 4;
else if(iDirOfCar == 21 || iDirOfCar == 41)
iDirOfCar = 1;
else if(iDirOfCar == 23 || iDirOfCar == 43)
iDirOfCar = 3;
}
}
void CPassCar::render()
{
pCarMesh->render();
}
74

Приложение Б

Результаты работы программы


75
76
77