воскресенье, 2 декабря 2012 г.

C++ сериализация данных

Иногда нужно сохранить состояние класса в файл, передать состояние класса по сети. Подобные задачи обычно решает сериализация.
Говоря о сериализации, я подразумеваю механизмы перевода некоторого класса, структуры или набора переменных в определённый формат (бинарный, текстовый, XML, HTML и т.д.), а также сам процесс перевода. Десериализация - процессы и механизмы, обратные сериализации (восстановление состояния из внешнего источника).
Самый простой способ, к которому чаще всего прибегают молодые программисты, - это простое копирование памяти в файл или еще куда-то. Т.е. берём указатель на класс/структуру/переменную и копируем N байт в файл. Пример:
.......
MyClass m;
..............
std::ofstream f("dump.bin", std::binary);
f.write(&m, sizeof(m));
f.close();
........................
Но этот метод сериализации имеет ряд ограничений и недостатков:
  • Допустимо использовать только для POD-структур (POD - Plain Old Data) и встроенных типов. Почему, будет понятно из следующих пунктов.
  • Если программистом описан конструктор, то компилятор вправе в класс добавить какие-то свои вспомогательные переменные, что превращает класс в не POD-структуру, на самом деле это не так страшно, но формально это так.
  • При сохранении указателей членов класса будут скопированы только адреса, хранимые указателями и, естественно, класс с указателями это не POD-тип
  • Если в классе объявленные виртуальные функции (или он унаследован от класса содержащего виртуальные функции), это приводит к тому, что класс будет дополнен указателем на таблицу виртуальных функций, и с этим указателем та же проблема, что и со всеми другими. Опять же не POD-тип.
  • Если ваш класс содержит внутри себя не POD типы или унаследован от не POD-типа, то ваш класс тоже не под тип, т.е. нет никакой гарантии, что копирование куска памяти позволит постановить состояние класса.
  • Различное выравнивание данных внутри класса может сделать невозможным перенос сохранённого класса на другую платформу или даже в программу, скомпилированную с другими параметрами компиляции.
  • Различный порядок байт не позволит переносить данные между такими платформами, как: x86 и PowerPC
  • И такая сериализация не даёт сохранить в удобочитаемой форме XML, текст или CSV
Есть много ситуаций, когда создание дампа памяти - вполне допустимый метод сериализации, но также есть другое множество ситуаций, когда такой подход недопустим.
Как только программист задумывается о сериализации данных, ему сразу же хочется выполнять сериализацию всего парой строк кода: легко и изящно, например, так:
// вывести состояние класса и всех его членов.
std::cout << myClass;
 
// загрузить состояние класса из XML
myXML >> myClass;
И, естественно, самый простой способ быстро добиться результата - это использовать "велосипед", написанный другими. "Велосипед" возьмём хороший, многофункциональный. Он умеет выполнять сериализацию и десериализацию стандартных контейнеров, классов, указателей, ссылок и еще чего-то. Также он умеет сохранять, работать с различными форматами выходных данных: бинарный, текст, XML. Если очень хочется, то он может сохранить не только в поток, но и куда угодно, например, в вектор или в сокет или выкинуть в пропасть.
Полное описание "велосипеда": http://www.boost.org/doc/libs/1_36_0/libs/serialization/doc/index.html
Вот пример использования (взято из описания):
/////////////////////////////////////////////////////////////
// gps координаты
//
// illustrates serialization for a simple type
//
class gps_position
{
private:
    friend class boost::serialization::access;
    // When the class Archive corresponds to an output archive, the
    // & operator is defined similar to <<.  Likewise, when the class Archive
    // is a type of input archive the & operator is defined similar to >>.
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & degrees;
        ar & minutes;
        ar & seconds;
    }
    int degrees;
    int minutes;
    float seconds;
public:
    gps_position(){};
    gps_position(int d, int m, float s) :
        degrees(d), minutes(m), seconds(s)
    {}
};
 
int main() {
    // create and open a character archive for output
    std::ofstream ofs("filename");
 
    // create class instance
    const gps_position g(35, 59, 24.567f);
 
    // save data to archive
    {
        boost::archive::text_oarchive oa(ofs);
        // write class instance to archive
        oa << g;
     // archive and stream closed when destructors are called
    }
 
    // ... some time later restore the class instance to its orginal state
    gps_position newg;
    {
        // create and open an archive for input
        std::ifstream ifs("filename");
        boost::archive::text_iarchive ia(ifs);
        // read class state from archive
        ia >> newg;
        // archive and stream closed when destructors are called
    }
    return 0;
}
Теперь по пунктам, как это работает:
  1. Создаём внутри нашего класса метод serialize, получаем ссылку на архив и номер версии(можно по-разному сериализовать в зависимости от версии), если метод приватный, то добавляем в друзья class boost::serialization::access. Метод serialize будет вызываться при сериализации и десериализации.
  2. Открываем файл и создаём архив (text_oarchive текстовый выходной архив), в нашем случае текстовый, архив - это тот самый класс, который выполняет основную работу.
  3. Вызываем всеми любимый оператор << - этот оператор вызывает метод serialize для классов или же внешние функции(они идут в комплекте) для встроенных типов и стандартных контейнеров.
  4. text_oarchive::operator<< вызвал наш метод serialize и передал вовнутрь себя, тут возникает вопрос: почему внутри serialize используется оператор &, а не <<? Ответ: потому что у выходного архива операторы & и << по сути это одно и тоже, у входного операторы & и >> - одно и тоже. Т.е. ничто не мешает в коде поменять "ia >> newg;" на "ia & newg;".
  5. Если нужно изменить метод сериализации, достаточно поменять тип архива (для XML архива придётся сделать еще некоторою работу в методе serialize).
На этом, в общем-то, работа по поддержке сериализации закончена.
При желании, можно разделить методы для сериализации и десериализации. Кстати, boost::seralization гарантирует, что порядок байт при сериализации будет изменён, если потребуется, так что можно спокойно передать long с x86 на PowerPC.
Немного поправленный пример использования и результаты работы:
#include "stdafx.h"
#include <iostream>
#include <fstream>
#include <string>
#include <fstream>
#include <vector>
 
// include headers that implement a archive in simple text format
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>
// включаем, чтобы сериализация работала с векторами
#include <boost/serialization/vector.hpp>
// включаем, чтобы нормально проходила сериализация XML
#include <boost/serialization/nvp.hpp>
 
class gps_position
{
private:
 friend class boost::serialization::access;
 
 template<class Archive>
 void serialize(Archive & ar, const unsigned int version)
 {
  // то же, что и make_nvp, только имя параметра выводится в макросе
  ar & BOOST_SERIALIZATION_NVP(degrees);
  ar & BOOST_SERIALIZATION_NVP(minutes);
  ar & BOOST_SERIALIZATION_NVP(seconds);
 }
 int degrees;
 int minutes;
 float seconds;
public:
 gps_position(){};
 gps_position(int d, int m, float s) :
 degrees(d), minutes(m), seconds(s)
 {}
};
 
template<typename TIArch, typename TOArch, typename TClass>
void TestArch(const std::string & file, int flags, const TClass & cont)
{
 
 { // Сериализуем
  std::ofstream ofs(file.c_str(), std::ios::out|flags);
  TOArch oa(ofs);
  // make_nvp создаёт пару имя-значение, которая отразится в XML
  // если не используем XML архив, то можно пару не создавать
  oa << boost::serialization::make_nvp("Test_Object", cont); 
 }
 
 TClass newg;
 { // Десериализуем
  std::ifstream ifs(file.c_str(), std::ios::in|flags);
  TIArch ia(ifs);
  ia >> boost::serialization::make_nvp("Test_Object",newg);
 }
 
 { // Еще раз сериализуем, чтобы потом сравнить результаты двух сериализаций
  // и убедиться, что десериализациия прошла корректно
  std::ofstream ofs((file+".tmp").c_str(), std::ios::out|flags);
  TOArch oa(ofs);
  oa << boost::serialization::make_nvp("Test_Object", cont);
 }
}
 
int _tmain(int argc, _TCHAR* argv[])
{
 std::ofstream ofs("filename");
 
 std::vector<gps_position> v;
 v.push_back(gps_position(35, 59, 24.567f));
 v.push_back(gps_position(36, 60, 25.567f));
 v.push_back(gps_position(37, 61, 26.567f));
 
 using namespace boost::archive;
 TestArch<text_iarchive, text_oarchive>("text_arch.dump", 0, v);
 TestArch<binary_iarchive, binary_oarchive>("binary_arch.dump", std::ios::binary, v);
 TestArch<xml_iarchive, xml_oarchive>("xml_arch.dump", 0, v);
 
 return 0;
}
Файл text_arch.dump:
22 serialization::archive 3 0 0 3 0 0 35 59 24.566999 36 60 25.566999 37 61 26.566999
Как видите, использование NVP никак не отразилось на внешнем виде архива.
Файл xml_arch.dump:
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="3">
<Test_Object class_id="0" tracking_level="0" version="0">
 <count>3</count>
 <item class_id="1" tracking_level="0" version="0">
  <degrees>35</degrees>
  <minutes>59</minutes>
  <seconds>24.566999</seconds>
 </item>
 <item>
  <degrees>36</degrees>
  <minutes>60</minutes>
  <seconds>25.566999</seconds>
 </item>
 <item>
  <degrees>37</degrees>
  <minutes>61</minutes>
  <seconds>26.566999</seconds>
 </item>
</Test_Object>
</boost_serialization>
Бинарный архив приводить не стану :) не очень красочно, но занимает он 79 байт, из которых 39 - заголовок и 40 - полезная информация.

За универсальность boost::serialization придётся заплатить:
  • Во время компиляции шаблоны могут разворачиваться довольно долго.
  • Скорость: стек вызовов для сериализации какой-нибудь не слишком больной структурки, может быть просто ужасающим - вызовов 20-30.
Но если вы не пишете систему массового обслуживания, то это то, что вам нужно, с помощью этой библиотеки можно даже реализовать маршалинг или RPC.

Надеюсь, в общих чертах понятно, как примерно работает сериализация и десериализация, и если понадобится, можно реализовать свою сериализацию.

Вот пример своей реализации архива, который я использую вместо boost:binary_iarchive (была ОЧЕНЬ важна скорость, а универсальность не очень, но для маршалинга хватает), делал так, чтобы можно было использовать один вместо другого без переделки кода:
class binary_iarchive
{
public:
 typedef serialization::container container;
 typedef container::iterator iterator;
 
 container &cont_;
 size_t currentPos_;
 typedef boost::mpl::bool_<false> is_saving;
 binary_iarchive(container & cont, long = 0)
   : cont_(cont)
   , currentPos_(0)
 {
 }
 
 template<typename T>
 binary_iarchive & operator>>(T & val)
 {
  deserialize_impl(val);
  return *this;
 }
 
 void reset()
 {
  resetPos();
  cont_.clear();
 }
 
 template<typename T>
 inline void raw_read(T beginPos, size_t len)
 {
  if (static_cast<size_t>(cont_.size() - currentPos_) < len)
   throw std::runtime_error("No more data");
 
  iterator pos = cont_.begin() + currentPos_;
  iterator endPos = pos + len;
  std::copy(pos, endPos, beginPos);
  currentPos_ = currentPos_ + len;
 }
private:
 // Fundamental
 template<typename T>
 inline void deserialize_impl(T & val, typename boost::enable_if<boost::is_fundamental<T> >::type* dummy = 0)
 {
  raw_read(reinterpret_cast<char*>(&val), sizeof(T));
 }
 
 //Classes
 template<typename T>
 inline void deserialize_impl(T & val, typename boost::enable_if<boost::is_class<T> >::type* dummy = 0)
 {
  deserialize_class(*this, val);
 }
};
Кое-что порезал, чтобы не расслаблялись :)

среда, 22 августа 2012 г.

Ogre3D Базовое руководство 1

Введение в основные принципы Ogre: объекты SceneManager, SceneNode и Entity.

КРАТКОЕ ВВЕДЕНИЕ

В этом руководстве мы познакомим вас с базовыми понятиями в Ogre: Менеджер сцены (SceneManager), Node (SceneNode) и Entity. В начале нашего обучения мы не будем углубляться в программирование, вместо этого мы сфокусируемся на главных понятиях и концепциях.

По мере изучения руководства вы будете понемногу добавлять код в ваше приложение и наглядно видеть результаты. Ничто не заменит реального опыта в программировании. Боритесь с желанием просто читать!


Необходимые знания

  • Это руководство подразумевает, что у вас есть знания программирования на C++, и вы в состоянии установить и скомпилировать приложение с помощью Ogre.
  • Это руководство также подразумевает, что вы создали проект, используя Ogre Wiki Tutorial Framework, либо вручную, используя CMake или Ogre AppWizard.

Для начала

Первоначальный код

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

Прочитайте Setting up an Application, чтобы понять как создать и настроить проект под Ogre.

Создайте новый проект. Назовите его "Tutorial".

Добавьте к нему шаблон:

КОД: ВЫДЕЛИТЬ ВСЁ
BaseApplication.h
BaseApplication.cpp
TutorialApplication.h
TutorialApplication.cpp


Вы можете скачать файлы здесь.
Или воспользуйтесь Ogre AppWizard.

В этом руководстве мы будем работать только с файлом TurtorialApplication.cpp и методом createScene().

TutorialApplication.cpp должен содержать следующий код (заголовочные комментарии удалены, чтобы не создавать путаницы):

КОД: ВЫДЕЛИТЬ ВСЁ
#include "TutorialApplication.h"

TutorialApplication::TutorialApplication(void)
{
}

TutorialApplication::~TutorialApplication(void)
{
}

//-------------------------------------------------------------------------------------
void TutorialApplication::createScene(void)
{
    Ogre::Entity* ogreHead = mSceneMgr->createEntity("Head", "ogrehead.mesh");

    Ogre::SceneNode* headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode();
    headNode->attachObject(ogreHead);

    // Set ambient light
    mSceneMgr->setAmbientLight(Ogre::ColourValue(0.5, 0.5, 0.5));

    // Create a light
    Ogre::Light* l = mSceneMgr->createLight("MainLight");
    l->setPosition(20,80,50);
}



#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include "windows.h"
#endif

#ifdef __cplusplus
extern "C" {
#endif

#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
    INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )
#else
    int main(int argc, char *argv[])
#endif
    {
        // Create application object
        TutorialApplication app;

        try 
        {
            app.go();
        } catch( Ogre::Exception& e ) {
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
            MessageBox( NULL, e.getFullDescription().c_str(), "An exception has occured!", MB_OK | MB_ICONERROR| MB_TASKMODAL);
#else
            std::cerr << "An exception has occured: " << e.getFullDescription().c_str() << std::endl;
#endif
        }

        return 0;
    }

#ifdef __cplusplus
}
#endif


Давайте скомпилируем и запустим программу, чтобы убедится что все настроено правильно. Используйте клавиши WASD для передвижения, и мышь чтобы осмотреться. Нажав клавишу Esc вы закроете приложение.

Решение проблем

Если у вас возникли проблемы при компиляции, еще раз прочитайте Setting Up An Application, для того чтобы убедится, что вы правильно настроили компилятор. Также взгляните в файл Ogre.log, там могут находиться полезные сведения. Еще вы можете поискать решение вашей проблемы на форумах посвященных Ogre. Вполне вероятно ваша проблема уже возникала у других. Если по вашей проблеме нет информации, прочитайте правила форума и не стесняйтесь спрашивать. Если вы хотите, чтобы вам быстро ответили, не ленитесь выложить файл Ogre.log, сообщения об ошибках, и/или отладочную информацию.

В следующих статьях не будет секции "Решение Проблем", поэтому обратите особое внимание на следующие абзацы.

Проблема с Message Box

Если вы используете Visual Studio с включенной в проекте поддержкой Unicode, может возникнуть такая ошибка:
КОД: ВЫДЕЛИТЬ ВСЁ
error C2664: 'MessageBoxW' : cannot convert parameter 2 from 'const char *' to 'LPCWSTR' Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast


Проблема в том, что функция MessageBox ожидает строку в формате Unicode (в нашем случае), а мы отправляем ей строку в ANSI. Чтобы поправить это, измените следующую строку:

КОД: ВЫДЕЛИТЬ ВСЁ
MessageBox( NULL, e.what(), "An exception has occured!", MB_OK | MB_IConerror | MB_TASKMODAL);


на эту:

КОД: ВЫДЕЛИТЬ ВСЁ
MessageBoxA( NULL, e.what(), "An exception has occured!", MB_OK | MB_IConerror | MB_TASKMODAL);


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

Причина в том что функция MessageBox ссылается на MessageBoxA (ANSI) или MessageBoxW (Widechar/Unicode), в зависимости от конфигурации вашего проекта. Мы исправим эту ошибку явно указав использовать ANSI формат функции.

Отсутствие файлов конфигурации или DLL

Если ваше приложение собралось но при запуске выдает ошибку в связи с отсутствием DLL библиотек или конфигурационных файлов (*.cfg), тогда вы вероятно забыли их скопировать из каталога OgreSDK. В Visual Studio, собранные .exe файлы обычно помещаются в каталоги [ProjectFolder]\bin\release и [ProjectFolder]\bin\debug для режима Release и Debug соответственно. Вы должны скопировать все *.dll и *.cfg файлы из папки OgreSDK в эти каталоги. То есть копируйте файлы из [OgreSDK]\bin\release в [ProjectFolder]\bin\release и из [OgreSDK]\bin\debug в [ProjectFolder]\bin\debug. Также отредактируйте resources.cfg и укажите в нем правильные пути. В следующей секции разберем это более детально.

Проблемы с плагинами или ресурсами

Убедитесь что plugins.cfg и resources.cfg находятся в том же каталоге, что и собранный .exe файл. Plugins.cfg указывает огру доступные библиотеки для рендеринга (Direct3D9, OpenGL). Resources.cfg используется ExampleApplication и указывает пути на текстуры, меши и скрипты. Оба этих файла текстовые, отредактируйте их и убедитесь что указанные в них пути ведут куда надо. В противном случае окно с настройками Ogre не найдет библиотек для рендера или возникнет ошибка на экране или в Ogre.log, что выглядит примерно так:

КОД: ВЫДЕЛИТЬ ВСЁ
Description: ../../Media/packs/OgreCore.zip - error whilst opening archive: Unable to read zip file


В таком случае откройте файл resources.cfg и измените пути, которые указывают на каталоги в папке Media находящуюся в папке Ogre. Заметим, что использовать переменные окружения, вроде $(SomeVariable), нельзя.

Невозможно запустить приложение в Visual Studio или CodeBlocks

Если вы используете Visual Studio или Visual C++, и у вас возникли проблемы с запуском компиляции приложения, проблема скорее всего в настройках дебаггера. Если вы запустили приложение из среды разработки (нажав кнопку Run, или Start debugging) и получили сообщение о отсутствующих конфигурационных файлах (*.cfg), значит рабочий каталог не установлен.

Решение этой проблемы зависит от того, какую версию Visual C++ вы используете, поэтому точного решения здесь нет, но основные шаги должны быть теми же самыми. Правый клик по вашему проекту в "solution explorer", (не по самому пункту "solution") и выбираем "properties". Где то в "Configuration Properties" должна быть опция "Debugging". Там выбираем поле "Working directory". Там указываете директорию, куда сохраниться скомпилированный .exe файл.

Если вы не представляете, что писать в этом поле, обратите внимание на поле "Command", которое также должно находится в "Debugging Options". Для примера, в Visual C++ 2003, поле "Command" должно содержать что то вроде "..\..\bin\$(ConfigurationName)\$(TargetFileName)". Для поля "Working Directory" мы копируем поле "Command" удалив $(TargetFileName). Таким образом в "Working Directory" будет "..\..\bin\$(ConfigurationName)". Содержимое этого поля зависит от того какую версию Visual C++ вы используете, а также от того чем вы собираете ваше приложение, так что перед этим обязательно проверьте что содержится в поле "Command". Проверьте что вы изменили "Working Directory" для обоих конфигураций, как Release, так и Debug.

В Visual C++ 2005 нужные поля вероятно будут располагаться немного по другому. Для начала попробуйте указать в этом поле каталог "..\..\bin\$(ConfigurationName)", если приложение все еще не запускается - поэкспериментируйте или попробуйте спросить совета на форумах посвященных Ogre.

Если вы используете Code::Blocks, тогда вам необходимо сделать то же самое. Правый клик по вашему проекту, выбираем "Properties...", переходим на вкладку "Build targets", и изменяем содержимое поля "Execution working dir:" указывая в нем тот же каталог в который помещаются исполняемые файлы (см. поле Output filename).

Причина по которой мы делаем это - Ogre ожидает, что некоторые файлы будут находится в том же каталоге, что и .exe файл. И без настройки рабочей директории (working directory), приложение не запустится.
____________

Итак вы запустили ваше приложение, теперь удалим весь код внутри функции createScene() оставив саму функцию. Мы будем писать код внутри этой функции и давать объяснение к каждой строке нашего кода.

Как работает Ogre

Довольно объемный раздел. Мы начнем со SceneManager и пойдем дальше, узнав что такое Entities и SceneNodes. Приложение на Ogre базируется на этих трех классах.

Основы SceneManager

Все что вы видите в окне вашего приложения, все управляется с помощью SceneManager. Когда вы добавляете объект на сцену, класс SceneManager берет на себя управление его позиционированием. Когда вы создаете объект Camera, чтобы видеть сцену (в следующих руководствах мы рассмотрим это), то за ним опять же следит SceneManager. Когда вы создаете объекты Plane, Billboards, Lights и все остальное, то SceneManager также берет на себя управление их положением.

Есть несколько типов SceneManager. Есть SceneManager предназначенный для отрисовки ландшафта (Terrain), есть для отрисовки BSP Maps и так далее. Вы можете увидеть несколько типов SceneManager перечисленных здесь. По мере прохождения руководства мы рассмотрим это более подробно.

Основы Entity

Entity - это один из типов объектов которые вы можете добавить к сцене. Entity представляет собой 3D объекты. Робот будет объектом класса Entity, рыба, ландшафт по которому перемещаются ваши персонажи - все это Entity (разве что ландшафт представляет собой очень большой Entity). Источники света, источники частиц, камеры объектами Entity не являются.

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

Основы SceneNode

Как уже упоминалось выше, класс SceneNode содержит информацию о положении и ориентации объектов которые к нему присоединены. Когда вы создаете объект Entity, он не будет отображаться на экране, пока вы не привяжете его к SceneNode. Но и SceneNode по себе не является объектом, отображающимся на экране. Только в том случае если вы создадите SceneNode и прикрепите к нему Entity (или другой объект), вы имеете шанс действительно что-то увидеть.

К одному SceneNode можно присоединить любое количество объектов. Давайте предположим что у вас есть персонаж который ходит по кругу, и вы хотите, чтобы он освещал окружающее пространство. Для того, чтобы сделать это, сначала нужно создать объект SceneNode, потом создадим Entity которое будет представлять собой нашего персонажа и прикрепим его к SceneNode. Затем мы создадим источник света (Light) и прикрепим его к той же SceneNode, что и персонажа. SceneNode также может быть прикреплена к другим SceneNodes что позволяет создавать иерархию (наследование классов). В следующем руководстве мы расскажем больше об использовании SceneNode.

Один важный момент касающийся SceneNodes. Позиция SceneNode всегда относительна к ее предку, и каждый SceneManager содержит корневой SceneNode к которому прикреплены остальные SceneNode.

Ваше первое приложение на Ogre

Вернемся к коду который мы создали ранее. Найдем функцию TutorialApplication::createScene. Как мы упоминали ранее, изменения будут происходить только в этой функции. Первое что мы сделаем, это установим уровень освещенности самой сцены (ambient light), чтобы видеть результаты нашего труда (в темноте же ничего не видно ;) ). Для этого вызываем функцию setAmbientLight и указываем нужный нам цвет. Конструктор ColorValue принимает три параметра (RGB) для красного, зеленого и голубого цветов. Значение колеблется в промежутке от 0 до 1. Добавим этот код к createScene:

КОД: ВЫДЕЛИТЬ ВСЁ
mSceneMgr->setAmbientLight(Ogre::ColourValue(1.0, 1.0, 1.0));


Следующее что мы сделаем, это создадим Entity. Вызываем метод createEntity объекта SceneManager:

КОД: ВЫДЕЛИТЬ ВСЁ
Ogre::Entity* ogreHead = mSceneMgr->createEntity("Head", "ogrehead.mesh");


У вас наверняка возникли некоторые вопросы. Первый откуда взялся mSceneMgr, и что за параметры мы передаем в функцию. Переменная mSceneMgr является ссылкой на текущий объект класса SceneManager (это для нас настроено в BaseApplication). Первый параметр функции createEntity это название объекта который мы создаем. Все экземпляры Entity должны иметь уникальное название. Так как при попытке создать два объекта с одинаковым именем, возникнет ошибка. Параметр "ogrehead.mesh" указывает меш (проще говоря сохраненную 3d модель), который будет использован для Entity. В нашем случае "ogrehead.mesh" есть в папке Ogre SDK, и оттуда меш и берется. Загрузка ресурсов будет описана в следующих статьях. Сейчас мы используем для загрузки ресурсов BaseApplication.

Итак Entity мы создали, теперь нам необходимо создать SceneNode, к которому мы прикрепим наш Entity. Каждый SceneManager имеет корневую SceneNode, мы создадим дочернюю к нему SceneNode.

КОД: ВЫДЕЛИТЬ ВСЁ
Ogre::SceneNode* headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode("HeadNode");


Это длинное выражение сперва вызывает getRootSceneNode текущего объекта SceneManager. Затем он вызывает метод createChildSceneNode корневой SceneNode. Параметр передаваемый createChildSceneNode это имя SceneNode которую мы создаем. Как и Entity, два экземпляра SceneNode не могут иметь одинаковые имена.

И наконец мы присоединяем Entity к SceneNode, для того чтобы указать голове огра место где она будет находиться:

КОД: ВЫДЕЛИТЬ ВСЁ
headNode->attachObject(ogreHead);


Вот и все! Скомпилируйте и запустите ваше приложение. Вы должны увидеть голову огра на вашем экране.
Изображение

Векторы и координаты

Перед тем как мы пойдем дальше, нужно понять что такое координаты на экране и как работают векторы в Ogre. Ogre (подобно многим графическим движкам) использует координатные оси X и Z для горизонтальных плоскостей, и Y для вертикальной координаты. Если ориентироваться по монитору, то ось X проходит слева направо, с правой стороны положительная часть оси. Ось Y проходит снизу вашего монитора на верх, сверху положительная часть оси. Ось Z берет свое начало глубоко в недрах монитора и выходит из него, вне монитора положительная часть оси.

Изображение

Заметим, что голова огра сейчас обращена прямо к нам по направлению оси Z, как так? На это влияют свойства самого меша. Камеры рассматриваются позже в следующем руководстве, но сейчас стоит учесть, что голова огра находится в начале координат (0,0,0), и мы видим ее спереди. Стандартное направление головы берется из самой модели, направление, которое было задано при ее создании. Ogre не может предполагать как ориентирована ваша модель. Каждый меш который мы загружаем может быть ориентирован по разному.

Для того, чтобы задать позицию и направление Ogre использует класс Vector (Вектор). Векторы определены в трех классах для 2 (Vector2), 3 (Vector3) и 4 (Vector4) измерений, из них чаще всего используется Vector3. Если вы незнакомы с векторами, стоит освежить их в памяти, прежде делать что нибудь серьезное. Незнание векторов станет серьезной проблемой, когда вы начнете делать что-либо серьезное.

Добавление другого объекта

Поняв как работает система координат, вернемся к нашему коду. Ни в одной из тех трех строк, что мы написали, не указано точное расположение головы огра. Большинство функций в Ogre используют для этого значения по умолчанию. Для примера функция SceneNode::createChildSceneNode имеет три параметра: название SceneNode, ее позиция и исходное положение (разворот). Позиция, как вы уже убедились, имеет координаты (0,0,0). Давайте создадим еще один объект SceneNode, но сейчас укажем ее начальное расположение:

КОД: ВЫДЕЛИТЬ ВСЁ
Ogre::Entity* ogreHead2 = mSceneMgr->createEntity( "Head2", "ogrehead.mesh" );
Ogre::SceneNode* headNode2 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "HeadNode2", Ogre::Vector3( 100, 0, 0 ) );
headNode2->attachObject( ogreHead2 );


Это должно быть уже знакомо. Мы сделали то же самое что и в прошлый раз, кроме двух вещей. Первая - мы назвали SceneNode и Entity немного по другому. Вторая - мы указали начальное положение смещенное на 100 единиц по оси X относительно корневой SceneNode (напомню что координаты SceneNode заданы относительно их предков). Скомпилируйте и запустите программу. Сейчас на сцене две головы огра расположенные бок о бок. Можно сдвинутся немного назад используя клавишу "S", или воспользоваться мышкой, чтобы их увидеть.

Подробней про Entity

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

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

Функция getName возвращает название объекта Entity, функция getParentSceneNode возвращает SceneNode к которой присоединен объект Entity.

Подробней про SceneNode

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

Для того чтобы получить или указать текущее расположение SceneNode есть функции getPosition и setPosition (они возвращают координаты относительно родительской SceneNode). Метод translate позволяет передвинуть объект относительно его текущей позиции.

Кроме позиционирования SceneNode позволяет вращать и масштабировать объект. Для масштабирования применяется функция scale. Для вращения относительно каждой из координатных осей используются функции pitchyaw и roll. Вы можете воспользоваться resetOrientation, чтобы отменить все вращение примененные к объекту. Вы также можете использовать функции setOrientationgetOrientation и rotate для более гибкого вращения. Тему Кватернионы (Quaternions) мы поднимем в более поздних статьях.

Вы уже знакомы с функцией attachObject. Эти связанные функции также будут довольно полезны, если вы ищете способ управлять объектами которые присоединены к SceneNode: numAttachedObjectsgetAttachedObject (есть несколько версий этой функции), detachObject (тоже несколько версий),detachAllObjects. Также существует целый набор функций для работы с предками и потомками SceneNode.

Поскольку все позиционирование и перемещение выполнятся относительно предка SceneNode, мы можем заставить две SceneNode двигаться вместе. Итак у нас получился такой код:

КОД: ВЫДЕЛИТЬ ВСЁ
mSceneMgr->setAmbientLight(Ogre::ColourValue(1.0, 1.0, 1.0));

Ogre::Entity* ogreHead = mSceneMgr->createEntity("Head", "ogrehead.mesh");
Ogre::SceneNode* headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode("HeadNode");
headNode->attachObject(ogreHead);

Ogre::Entity* ogreHead2 = mSceneMgr->createEntity( "Head2", "ogrehead.mesh" );
Ogre::SceneNode* headNode2 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "HeadNode2", Ogre::Vector3( 100, 0, 0 ) );
headNode2->attachObject( ogreHead2 );


Если мы изменим 6 линию с этой:

КОД: ВЫДЕЛИТЬ ВСЁ
Ogre::SceneNode* headNode2 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "HeadNode2", Ogre::Vector3( 100, 0, 0 ) );


на эту:

КОД: ВЫДЕЛИТЬ ВСЁ
Ogre::SceneNode* headNode2 = headNode->createChildSceneNode( "HeadNode2", Ogre::Vector3( 100, 0, 0 ) );


То укажем, что headNode2 является потомком headNode. При перемещении headNode, headNode2 также будет перемещатся, но при передвижении headNode2, headNode останется на месте. Для примера этот код будет перемещать только headNode2:

КОД: ВЫДЕЛИТЬ ВСЁ
headNode2->translate( Ogre::Vector3( 10, 0, 10 ) );


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

КОД: ВЫДЕЛИТЬ ВСЁ
headNode->translate( Ogre::Vector3( 25, 0, 0 ) );


Этот код заставит перемещаться headNode, но учитывая то что headNode2 является потомком headNode, headNode2 будет перемещаться вместе с headNode. Если у вас возникли проблемы с пониманием этого, давайте разберем простой пример. Мы начнем с корневой SceneNode и пойдем вниз по иерархии SceneNode. Давайте предположим (возьмем наш случай), мы начнем с headNode (0, 0, 0) и переместим ее (translate) на (25, 0, 0). Таким образом новая позиция headNode станет (25, 0, 0) относительно его предка. Позиция headNode2 (100, 0, 0) мы перемещаем ее на (10, 0, 10). Итого новая позиция headNode2 станет (110, 0, 10) относительно его предка (headNode).

Теперь представим где будут находится эти SceneNode. Начнем с корневой SceneNode. Ее позиция всегда (0, 0, 0). Сейчас положение headNode = (root + headNode): (0, 0, 0) + (25, 0, 0) = (25, 0, 0). Ничего удивительного.

headNode2 является потомком headNode, таким образом ее позиция будет (root + headNode + headNode2): (0, 0, 0) + (25, 0, 0) + (110, 0, 10) = (135, 0, 10).

Это всего лишь пример того как наследуются позиции SceneNode. Впрочем необходимость вычислять абсолютные позиции SceneNode возникает довольно редко. Под конец заметим, что вы можете получить как объекты SceneNode так и Entity используя методы getSceneNode и getEntity объекта SceneManager. Следовательно нет необходимости хранить на них ссылки. Имеет смысл сохранять ссылки только на те объекты, которыми вы часто пользуетесь.

Дополнительные рекомендации

К этому моменту у вас должно сложится общее представление о Entities, SceneNodes и SceneManager. Мы предлагаем начать с кода, расположенного выше, и добавлять и удалять головы огра со сцены. Как только вы закончите "баловаться", удалите все содержимое функции createScene и давайте поэкспериментируем со следующими фрагментами кода:

Масштабирование (Scale)

Чтобы масштабировать модели, воспользуемся методом SceneNode->scale. Пропробуйте изменять значения которые мы передаем в функцию scale, и посмотрите, что у вас получится:

КОД: ВЫДЕЛИТЬ ВСЁ
mSceneMgr->setAmbientLight(Ogre::ColourValue(1.0, 1.0, 1.0));

Ogre::Entity* ogreHead = mSceneMgr->createEntity("Head", "ogrehead.mesh");
Ogre::SceneNode* headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode("HeadNode");
headNode->attachObject(ogreHead);

headNode->scale( .5, 1, 2 ); 

Ogre::Entity* ogreHead2 = mSceneMgr->createEntity( "Head2", "ogrehead.mesh" );
Ogre::SceneNode* headNode2 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "HeadNode2", Ogre::Vector3( 100, 0, 0 ) );
headNode2->attachObject( ogreHead2 );

headNode2->scale( 1, 2, 1 );


Изображение

Вращение (Rotations)

Вы можете вращать объект используя функции yaw, pitch, и roll передавая им в качестве параметров угол вращения в градусах или радианах. В огре это объекты Degree и Radian соответственно. pitch это вращение вокруг оси X, yaw вращает объект вокруг оси Y а roll - вокруг оси Z. Используйте правую руку для удобства: поместите ваш палец в направлении оси, сожмите оставшиеся пальцы. Направление согнутой части руки - положительное вращение вокруг оси:

Изображение

Попробуем изменить значение объекта Degree, которое мы передаем и объединим несколько трансформаций:

Изображение

КОД: ВЫДЕЛИТЬ ВСЁ
mSceneMgr->setAmbientLight(Ogre::ColourValue(1.0, 1.0, 1.0));

Ogre::Entity* ogreHead = mSceneMgr->createEntity("Head", "ogrehead.mesh");
Ogre::SceneNode* headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode("HeadNode");
headNode->attachObject(ogreHead);

headNode->yaw( Ogre::Degree( -90 ) );
  
Ogre::Entity* ogreHead2 = mSceneMgr->createEntity( "Head2", "ogrehead.mesh" );
Ogre::SceneNode* headNode2 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "HeadNode2", Ogre::Vector3( 100, 0, 0 ) );
headNode2->attachObject( ogreHead2 );

headNode2->pitch( Ogre::Degree( -90 ) );
  
Ogre::Entity* ogreHead3 = mSceneMgr->createEntity( "Head3", "ogrehead.mesh" );
Ogre::SceneNode* headNode3 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "HeadNode3", Ogre::Vector3( 200, 0, 0 ) );
headNode3->attachObject( ogreHead3 );

headNode3->roll( Ogre::Degree( -90 ) );


В Microsoft Visual Studio 2010 может возникнуть ошибка "Degree is an undeclared identifier". Это может быть исправлено с помощью вызова функции Ogre::Degree, позволяя компилировать вращение.

Среда разработки в Ogre

Большинство файлов (.dll и .cfg) на которые мы ссылаемся в этом туториале можно найти в каталоге OgreSDK/bin версии release и debug. Ваши программы в режиме Debug должны использовать файлы из каталога debug OgreSDK, в режиме Release - файлы из каталоге release соответственно.

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

Библиотеки (.dll) и плагины

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

Ogre разделен на 3 большие группы библиотек: основная библиотека, плагины и внешние библиотеки.

Основная библиотека

Основная группа библиотек состоит из самой библиотеки и библиотек которые она использует. Основная библиотека Ogre - OgreMain.dll. Эта dll требует некоторые другие библиотеки, такие как cg.dll. Эти библиотеки использует каждое приложение Ogre без исключений.

Плагины

Вторая группа библиотек это плагины. Довольно большая часть функциональности Ogre вынесена в подключаемые библиотеки и может быть включена или выключена в зависимости от потребностей. Базовые плагины которые поставляются вместе с Ogre имеют в названии файла префикс "Plugin_". Также вы можете создавать собственные плагины если у вас есть в этом необходимость, но в этом руководстве этой темы мы не коснемся. Для системы рендеринга (OpenGL, DirectX) Ogre также использует плагины. Эти плагины имеют префикс "RenderSystem_". Эти плагины существуют для того, чтобы вы могли добавлять или удалять системы редеринга из вашего приложения. Особенно полезно это если вы пишете шейдеры или что нибудь зависимое от (для примера) OpenGL и вам необходимо запретить возможность запускать приложение под DirectX. В таком случае можно просто удалить соответствующий плагин RenderSystem. В дополнение, если вашей целью является разработка под нестандартную платформу, вы можете написать собственный плагин для RenderSystem, но в нашем руководстве мы этого не коснемся. В следующей секции будет рассказано как удалить плагины.

Внешние и вспомогательные библиотеки

Третья группа библиотек это внешние библиотеки. Ogre сам по себе является просто библиотекой для рендеринга. Он не занимается отрисовкой пользовательского интерфейса, обработкой сообщений от мыши и клавиатуры, обработкой физики и так далее. Для всего этого вам необходимо использовать другие библиотеки.

Демонстрационные приложения Ogre и SDK версия Ogre включают в себя некоторые такие библиотеки.

- Обработку сообщений клавиатуры и мыши берет на себя OIS (система ввода). Эта библиотека содержится в файле OIS.dll.
- Cg ("C" для графики), используемая CgProgramManager, находится в Cg.dll.

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

Мораль истории

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

Если ваше приложение не использует Cg ProgramManager но использует OIS, тогда нет нужды включать в дистрибутив библиотеки от Cg и CgProgramManager, но о библиотеке OIS необходимо позаботится, иначе приложение не запуститься.

Файлы конфигурации

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

plugins.cfg
Файл содержит ссылки на плагины которые использует приложение. Чтобы добавить или удалить плагин из приложения просто отредактируйте этот файл. Удалить плагин можно убрав из этого файла соответствующую строку или закомментировав ее добавив # в начале строки. Включить плагин можно добавив строку вида "Plugin=[имя_плагина]". Не нужно дописывать ".dll" в конце названия плагина. Префиксы "RenderSystem_" или "Plugin_" тоже не надо указывать. Вы также можете указать каталог где находятся плагины изменив параметр "PluginFolder". Можно указывать как абсолютные так и относительные пути, но переменные окружения типа $(SomeVariable) использовать нельзя.

resources.cfg
Файл содержит список каталогов в которых Ogre ищет ресурсы. Ресурсы включают в себя скрипты, 3d объекты, текстуры и так далее. В этом файле аналогично можно указывать как абсолютные так и относительные пути, но нельзя переменные окружения типа $(SomeVariable). Заметим что Ogre не ищет ресурсы в подкаталогах, так что придется прописать путь к каждой из папок. К примеру если у вас есть иерархия каталогов вроде "res\meshes" и "res\meshes\small", в ресурсный файл придется добавить две строки содержащие пути к обоим каталогам.

media.cfg
В файле содержится более детальная информация о ресурсах. Не думаю что сейчас у вас будет необходимость изменять этот файл, так что мы опустим детали. Больше информации ищите на форумах посвященных Ogre.

ogre.cfg
Это сгененрированый Ogre файл конфигурации экрана. Этот файл хранит ваши индивидуальные настройки графики. Этот файл не стоит передавать с вашей программой, у других возможно будут совсем другие настройки. Кстати не стоит редактировать его руками, оставьте эту задачу окну конфигурации Ogre.

quake3settings.cfg
Используется BSPSceneManager. Сейчас мы не используем этот файл, поэтому его описание мы пропустим. Этот файл тоже нет смысла распространять если вы не используете BSPSceneManager, но если даже используете, то его содержимое может полностью различатся в зависимости от требований вашего приложения.

Это были все конфигурационные файлы, которые используются непосредственно Ogre. Для запуска Ogre необходимы файлы "plugins.cfg", "resources.cfg", и "media.cfg". В следующих руководствах мы расскажем больше о том как изменить их расположение и какие дополнительные возможности они предоставляют.

Заключение

На этот момент у вас уже должно сложится общее представление о классах SceneManager, SceneNode и Entity. Вы не должны быть знакомы со всем функциями, которые мы упоминали. Так как это базовые объекты и мы будем использовать их довольно часто. В следующих руководствах вы получите о них более полное представление. Вы также познакомились с тем как настраивать Ogre в вашем проекте.