[ AmberSkyNet VR ]

Наш движок уже что-то умеет, но это всё приходится формировать руками, в прикладной программе... Хорошо бы научить наш класс мира сохранять создаваемые сцены в файл и загружать их оттуда. Вот этим сейчас и займёмся... Хранить всё будем в XML- формате, т.к. древовидная структура сцены и система параметров неплохо накладываются на него. Для работы я буду использовать tinyXML, но все-таки напишу отдельный плагин парсера данных, который вызовы этой библиотеки скроет в своих недрах под неким абстрактным интерфейсом.

IDataParser и его реализация СDataParserXML

IDataParser - интерфейсный класс, который будет обеспечивать некий универсальный интерфейс для доступа к древовидным структурам. Для внешней программы под ним может скрываться любой парсер. При желании можно написать парсер и ini-файлов, и конфигурационных файлов с фигурными скобками, итп.. Но я пока ограничился только XML.

typedef void* DataLeaf;

class IDataParser {
public:
    virtual ~IDataParser(){}

    virtual DataLeaf StartParse(void *data)=0; // начало парсинга
    virtual void EndParse()=0; // конец парсинга

    // функции разбора данных
    virtual const char *GetLeafName(DataLeaf data)=0;
    virtual DataLeaf GetFirstChildLeaf(DataLeaf data)=0;
    virtual DataLeaf GetNextSiblingLeaf(DataLeaf data)=0;
    virtual asn_List& GetParamList(DataLeaf data)=0;

    // функции формирования данных
    virtual DataLeaf CreateRootLeaf()=0;
    virtual bool SaveDataTree(const char * FileName )=0;
    virtual DataLeaf AddLeafWithName(DataLeaf parent, const char *LeafName)=0;
    virtual bool SetParam(DataLeaf data, const char * Name, const char * Value )=0;
    
};

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

Функцию EndParse использовать не обязательно, но она предназначена для окончания работы парсера с данными, полученными через StartParse.

Функция GetLeafName выдаёт имя нашего узла leaf.

Функция GetFirstChild выдаёт первого "потомка" узла leaf.

Функция GetNextSiblingLeaf выдаёт следующего "потомка" одного уровня с узлом leaf.

Функция GetParamList выдаёт список параметров нашего узла leaf.

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

Функция SaveDataTree формирует файл, в который сбрасывает нашу структуру.

Функция AddLeafWithName присоединяет к узлу leaf новый узел с именем LeafName.

Функция SetParam добавляет узлу leaf параметр с именем Name и значением Value.

Реализация класса СDataParserXML пытается обеспечить этот интерфейс, используя вызовы tinyXML. Все остальные парсеры я пока писать не стал. Потом.. может быть.. посмотрим.. Хотя, смысл?

Модификация IWorld и CWorldSimple

В интерфейс мира добавим пару функций:

bool LoadWorld(const std::string& FileName)
bool SaveWorld(const std::string& FileName)

В классе CWorldSimple функция SaveWorld используя рекурсивную процедуру Node2Leaf (см. файл CWorldSimple.cpp) формирует структуру данных по всем узлам, начиная с корневого, копируя параметры узлов дерева сцены в структуру данных, сохраняя порядок их вложенности друг в друга.
Получаемое из узла дерева сцены имя реализации базового класса ( через GetType() ) записывается как параметр с именем "Type".
Значения позиции, размеров, итп.. узлов сцены, получаемые через соотвествующие функции (getPos(), GetSize()..) переводятся из CVector в String и тоже записываются как параметры узла c именами "Pos", "Size", итп.
После отображения дерева сцены в структуре данных сбрасываем полученную структуру на жесткий диск.

Функция LoadWorld загружает файл с диска и начинает его разбор. На выходе получаем корневой узел. А потом при помощи рекурсивной функции Leaf2Data класса CDataParserXML формируем сцену, согласно порядку вложенности в загруженном файле.
Получаем из текущего узла leaf список параметров через GetParamList.
Читаем параметр "Type" и просим систему плагинов сформировать нам экземпляр класса такого типа.
А потом через SetParam устанавливаем параметры из текущего leaf нашему созданному экземпляру класса.
Параметры "Pos", "Size", итп. переводятся из String в CVector и передаются в созданный экземпляр класса через соответствующие функции ( setPos(), setSize()... ).
После чего получаем из текущего leaf список дочерних узлов, формируем их и подсоединяем к текущему leaf.

Например, если прочитанное из узла структуры данных значение параметра "Type" равно "Node3ds" мы просим систему плагинов сформировать нам экземпляр класса Node3ds. А потом устанавливаем ему остальные параметры из того же узла- в их числе будет и имя 3ds-модели. Класс Node3ds отслеживает изменение этого параметра и автоматически обновит свою внутренную структуру. Через функции setPos, setRot, и др. 3ds модель будет ориентирована в пространстве в том же самом положении, которое было у ней прежде, на момент сохранения сцены в файл.

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

ICamera и реализация CCamera

На предыдущем шаге код, реализующий камеру у нас находился в прикладной программе. Практически "вручную" вычислялось перемещение, движение и повороты камеры, которой и не было фактически. Было бы удобно создать класс, который бы упростил нашу прикладную программу, взяв на себя реализацию класса камеры.
Возможно, имеет смысл наследовать камеру от INode, или INode придать функционал камеры или создать класс модификатора, реализующий камеру, ну, я думаю, это впереди. Для начала - чего-нибудь попроще создать, чтобы работало... Как всегда, сначала составляем интерфейс, наследуемый от IBaseObject.


class ICamera: public virtual IBaseObject {
public:

virtual void lookAt(const CVector& Pos)=0;

virtual void View()=0;

virtual void MoveForward(const float val)=0;
virtual void MoveStrafe(const float val)=0;
virtual void MoveUp(const float val)=0;

virtual void setPos(const CVector& Pos)=0;
virtual const CVector& getPos() const =0;

virtual void setRotX(const float val)=0;
virtual void setRotY(const float val)=0;

virtual void getRot(float& RotX, float& RotY) const =0;

virtual void lookAtMouse(int Pos_X, int Pos_Y)=0;

};

Функция lookAt заставляет посмотреть камеру в точку пространства с координатами Pos.

Функция View устанавливает позицию точки обзора камеры и угол наклона вектора обзора камеры. Фактически, при её вызове мы получаем на экране вид из камеры.

Функции MoveForward, MoveStrafe, MoveUp двигают позицию камеры по углу зрения и перпендикулярно углу зрения "вправо" и "вверх" на расстояние val. Если задавать отрицательные значения val, то получим обратные движения - "назад", "влево", "вниз".

Функции setPos, getPos - устанавливают и читают значение позиции камеры.

Функция getRot позволяет читать два угла поворота камеры, а функции setRotX, seRotY устанавливают их. Первый угол - угол поворота камеры в горизонтальной плоскости (0 - 359), второй - угол наклона камеры по вертикальной плоскости (-90 - +90).

Функция lookAtMouse реализует функцию обзора пространства камерой при помощи "мышки". В неё передаются позиции курсора мышки по X и Y.

Чтобы реализовать простой на первый взгляд код класса CCamera я посмотрел довольно много разных исходников движков (например - Linderdaum, Frustum, Irrlight), демок от NeHe, исходников игровых программ. В общем, вроде как заработало то, что хотел и так, как хотел :)

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

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

При вызове функции View мы вычисляем матрицу по координатам камеры Pos и вектору направления камеры Dir, т.е. как бы заставляем смотреть нашу камеру в точку с координатами Pos+Dir. После чего через вызов DEVICER->SetModelMatrix() устанавливаем эту матрицу как матрицу вида для нашего окна.

Когда класс камеры был готов, он использовал команды glLoadMatrix, glMultMatrix, которые принадлежат библиотеке OpenGL, поэтому класс камеры был размещен в плагине DeviceGL. После того, как в интерфейс IDevice и соотвественно класс реализации CDeviceGL были добавлены функции, реализующие вызовы glLoadMatrix, glMultMatrix, надобность использования OpenGl в классе CCamera отпала - теперь он реализует данные вызовы через функции менеджера графики CDeviceGL. Но пока из класса плагина CDeviceGl в отдельный плагин я его не стал выносить. Потом - посмотрим...

Изменения в исходниках

include/:BaseObject скрыт в src/Common, классы движка наследуются от него через виртуальное наследование. Добавлен интерфейс IDataParser.

src/asnCommon/:Добавлены классы кватерниона и матрицы.
Класс CNode переделан на работу с матрицами. При отрисовке объекта вместо вызова команд поворотов и переноса теперь вызывается команда DEVICER->MultModelMatrix().

src/asnNode3ds/:Класс модицифирован с учётом возможности работать напрямую с матрицами, аналогично CNode.

src/asnDataParser/: новый плагин - плагин реализации парсера CDataParserXML.

src/asnDeviceGl/: в плагин добавлен класс CCamera. Немного обновлён класс CDeviceGl с учётом изменений в IDevice - изменения касаются более удобной работы с матрицами проекции, вида а также с добавлением возможности отслеживания комбинаций клавиш с нажатиями Alt, Ctrl и др.

src/asnMain/: небольшая демка с моделями 3ds, позаимствованными из проекта S.C.O.U.R.G.E.
Добавлены элементы CAD-системы 8) - можно добавлять предметы на сцену, удалять их, изменять их размеры, позицию, углы поворота.. Сохранять сцену в файл World.xml и загружать из него 8). А файл World.xml вполне можно подредактировать руками в любом текстовом редакторе.

src/asnWorld/: Добавлена возможность загрузки/сохранения сцены.

Исходники этого шага выложены в SVN. Скачать их можно набрав команду:

svn co https://svn.sourceforge.net/svnroot/ambernet/tags/AmberSkyNet-0.9 ambernet_0.9
Powered by: SourceForge.net Logo