Pull to refresh

Реалистичный пейзаж в Ogre 3D

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

Забегая вперёд, конечный результат выглядит так:







Необходимые библиотеки включают в себя:
  • Caelum для атмосферных эффектов, а именно неба и облаков;
  • Hydrax для водной поверхности;
  • Paged Geometry для растительности, включая деревья, кусты и траву, но не ограничиваясь ими.

Шаг первый: скелет приложения


В качестве скелета приложения будем использовать слегка модифицированный Ogre Wiki Tutorial Framework — общий каркас для руководств из огр-вики, так как он достаточно гибок для создания приложений небольшой сложности; кроме того, он включает в себя довольно много функционала, в частности, движение камеры по клавишам WASD, обзор мышью, счётчик FPS и другие полезные вещи. Для компиляции я использовал Microsoft Visual C++ 2008 и систему сборки CMake (последняя позволит скомпилировать проект под любой архитектурой, поддерживаемой Ogre).
Все исходники я выложил на Github (впрочем, чтобы не возиться с git, их можно можно скачать по этому адресу). Для компиляции понадобится заранее собранный 2008-ой студией Ogre ветки 1.7 и CMake; для того, чтобы последний правильно определил местонахождение Ogre, необходимо добавить переменную окружения OGRE_HOME, равную каталогу, в который он установлен.
Кроме того, для запуска тестового приложения понадобятся два архива: media.zip с текстурами, моделями и прочими нужными вещами, его нужно будет распаковать прямо в каталог проекта; и configs.zip, содержащий файлы конфигурации Ogre — его нужно распаковать в каталог сборки.
Для компиляции проекта сначала запускаем CMake GUI и в поле «Where is the source code» выбираем каталог с проектом, а в поле «Where to build the binaries» — произвольный, например, можно выбрать подкаталог build каталога проекта. Затем нажимаем кнопку Configure, выбираем Visual Studio 2008 в списке компиляторов, и если всё прошло нормально и CMake не ругнулся на отсутствие чего-либо, нажимаем Generate:



В результате чего в каталоге build появляется файл-решение Visual Studio LandscapeApp.sln, с которым можно непосредственно работать — вносить изменения в код, компилировать и запускать тестовый проект.
Собственно, после компиляции и запуска нас приветствует стандартное окно выбора подсистемы рендеринга Ogre:



Выбрав, например, Direct3D9 Rendering Subsystem в качестве таковой, нажимаем OK и наблюдаем чёрное окно с сиротливым счётчиком FPS, который явно зашкаливает — ещё бы, сцена-то пустая. К слову, счётчик можно свернуть/развернуть клавишей F, чтобы не мешался.



Что ж, как говорил один известный политик, «Главное — на́чать!»

Шаг второй: небо


Caelum — распространяемый по лицензии GNU Lesser GPL плагин к Ogre для создания фотореалистичных атмосферных эффектов, таких, как цвет неба, солнце, луна и звёзды с учётом текущего времени и координат наблюдателя, облака, погодные феномены (дождь, снег, туман) и т.д.
Скачаем последнюю версию Caelum из репозитория на Google Code, ибо, судя по сообщениям на форуме, у этого проекта появился новый мейнтейнер и наблюдается некая движуха — в проект добавляют новые фичи и исправляют старые ошибки:

hg clone https://code.google.com/p/caelum

Чтобы было удобнее, я скопировал все нужные исходники в дерево тестового проекта, внеся небольшие правки для исправления предупреждений компилятора. Авторы Caelum используют систему сборки CMake, поэтому его добавление в нашу систему сборки ограничивается строчкой
add_subdirectory(Caelum)

в файле CMakeLists.txt, добавлением подкаталогов Caelum/main/include и ${CMAKE_BINARY_DIR}/Caelum/main/include в список включаемых каталогов через команду include_directories, и добавлением Caelum в список линкуемых библиотек через команду target_link_libraries:
Код
include_directories( ${OIS_INCLUDE_DIRS}
${OGRE_INCLUDE_DIRS}
${OGRE_SAMPLES_INCLUDEPATH}
"Caelum/main/include"
"${CMAKE_BINARY_DIR}/Caelum/main/include"
# ...
target_link_libraries(LandscapeApp ${OGRE_LIBRARIES} ${OIS_LIBRARIES} Caelum)


В самом коде потребуется чуть больше изменений. Для начала включим заголовочный файл Caelum в наш главный заголовочный файл LandscapeApplication.hpp:
#include <Caelum.h>

и включим соответствующее пространство имён в файле исходного кода LandscapeApplication.cpp:
using namespace Caelum;

Далее, сам по себе Caelum — набор отдельных компонентов для рендеринга неба и атмосферы, которые управляются корневым классом CaelumSystem. Этот класс контролирует все компоненты и обновляет (update) их внутреннее состояние при надобности. Создадим экземпляр этого класса, для этого добавим указатель на него в наш корневой класс LandscapeApplication:
Код
class LandscapeApplication : public BaseApplication
{
// ...
protected:
virtual void createScene(void);
Caelum::CaelumSystem* mCaelumSystem;
};


И вызовем конструктор в методе createScene:
void LandscapeApplication::createScene(void)
{
mCaelumSystem = new CaelumSystem(mRoot, mSceneMgr, CaelumSystem::CAELUM_COMPONENTS_DEFAULT);

Наиболее важен последний параметр, являющийся битовой маской, указывающей, какие компоненты Caelum мы будем использовать. CAELUM_COMPONENTS_DEFAULT — набор по умолчанию, он включает все стабильные компоненты.
CaelumSystem и его компоненты имеют огромное количество настроек, большая часть которых доступна из кода в виде методов get/set и описана в документации; другой, более очевидный способ тюнинга — 3D-редактор Ogitor, созданный специально для Ogre и поддерживающий все описанные в данной статье библиотеки (и некоторые не описанные):



Финальный штрих — для изменения атмосферных эффектов во времени необходимо обновлять Caelum каждый фрейм; для этого достаточно добавить наш экземпляр CaelumSystem в списки Frame Listener'ов и Render Target Listener'ов Ogre:
mCaelumSystem = new CaelumSystem(mRoot, mSceneMgr, CaelumSystem::CAELUM_COMPONENTS_DEFAULT);
mRoot->addFrameListener(mCaelumSystem);
mWindow->addListener(mCaelumSystem);

Вуаля — нас встречает синее небо с медленно клонящимся к закату солнцем, редкие облака и серый туман:



Подправим некоторые параметры Caelum для большей зрелищности:
Код
// ...
mWindow->addListener(mCaelumSystem);

mCaelumSystem->getUniversalClock()->setTimeScale(100); // ускорим смену времени суток
FlatCloudLayer* cloudLayer = // добавим ещё один слой облаков,
mCaelumSystem->getCloudSystem()->createLayer(); // зададим:
cloudLayer->setCloudCover(0.8f); // плотность
cloudLayer->setCloudSpeed(Vector2(0.0001f, 0.0001f)); // скорость
cloudLayer->setHeight(5000); // и высоту


В итоге небо выглядит так:







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

Шаг третий: земля


Воспользуемся родной огровской подсистемой рендеринга местности (Ogre Terrain System) для добавления в наше приложение земной поверхности. Эта система была создана в 2009-ом году создателем Ogre Стивом Стритингом на смену устаревшим аддонам для рендеринга местности и является полностью отдельным опциональным компонентом (тем не менее, с версии 1.7 входящим в поставку Ogre). Ogre Terrain System включает в себя всё необходимое как для создания небольших тестовых приложений вроде нашего, так и для огромных миров, загружаемых в оперативную память отдельными небольшими кусочками-страницами — подробнее об этом можно почитать на огр-форуме, например, здесь; более того, один из проектов GSoC'11 — улучшение механизма страничной загрузки местности.
Первым делом, дадим знать CMake о наших намерениях: в файле CMakeLists.txt добавим ${OGRE_Terrain_LIBRARIES} в список линкуемых библиотек через команду target_link_libraries:
target_link_libraries(LandscapeApp ${OGRE_LIBRARIES} ${OGRE_Terrain_LIBRARIES} ${OIS_LIBRARIES} Caelum)

Теперь добавим в метод LandscapeApplication::createScene код для загрузки одной страницы местности (взятой мною из примера «Terrain» в составе Ogre), почти совпадающий с кодом из Ogre Basic Tutorial 3:
Код
mCamera->setPosition(Vector3(1683, 50, 2116)); // направляем камеру
mCamera->lookAt(Vector3(1963, 50, 1660));

Vector3 lightdir(0.55f, -0.3f, 0.75f); // свет для статического освещения местности
lightdir.normalise();
Light* light = mSceneMgr->createLight("tstLight");
light->setType(Light::LT_DIRECTIONAL);
light->setDirection(lightdir);
light->setDiffuseColour(ColourValue::White);
light->setSpecularColour(ColourValue(0.4f, 0.4f, 0.4f));

mTerrainGlobals = OGRE_NEW TerrainGlobalOptions; // глобальные настройки местности
mTerrainGroup = OGRE_NEW TerrainGroup(mSceneMgr, // группа страниц местности
Terrain::ALIGN_X_Z, 513, 12000);
mTerrainGroup->setOrigin(Vector3::ZERO);
mTerrainGlobals->setLightMapDirection(light->getDerivedDirection());
mTerrainGlobals->setCompositeMapAmbient(mSceneMgr->getAmbientLight());
mTerrainGlobals->setCompositeMapDiffuse(light->getDiffuseColour());
mSceneMgr->destroyLight("tstLight");

mTerrainGroup->defineTerrain(0, 0); // обозначаем наше намерение загрузить страницу с координатой (0, 0)
mTerrainGroup->loadAllTerrains(true); // собственно, загружаем все запрошенные страницы в синхронном режиме
mTerrainGroup->freeTemporaryResources(); // прибираемся


И, само собой, добавим в наш главный класс указатели mTerrainGlobals и mTerrainGroup:
Код
#include <Terrain/OgreTerrain.h>
#include <Terrain/OgreTerrainGroup.h>

class LandscapeApplication : public BaseApplication
{
// ...
protected:
// ...
Ogre::TerrainGlobalOptions* mTerrainGlobals;
Ogre::TerrainGroup* mTerrainGroup;
};


Если не заданы особые параметры, то страница с координатой (0, 0) будет загружаться из файла terrain_00000000.dat, который я и добавил в вышеупомянутый архив media.zip.
Результат не заставляет себя долго ждать:





Шаг четвёртый: вода


Наиболее широко распространённая библиотека для рендеринга водной поверхности с использованием проекционной сетки в Ogre — Hydrax, распространяется по лицензии GNU Lesser GPL.
Качаем отсюда (ссылка внизу первого поста) архив, для дальнейшей работы нужен будет каталог Hydrax-v0.5.1/Hydrax/src/Hydrax из него, который я и скопировал в дерево тестового проекта.
К сожалению, Hydrax не поддерживает систему сборки CMake, поэтому пришлось написать файл Hydrax/CMakeLists.txt для неё, который, впрочем, лишь чуть сложнее простого перечисления исходников и заголовочных файлов. Для интеграции Hydrax в систему сборки в основном CMakeLists.txt нужно добавить следующее:
Код
include_directories(
# ...
"Hydrax"
)
add_subdirectory(Hydrax)
# ...
target_link_libraries(LandscapeApp ${OGRE_LIBRARIES} ${OGRE_Terrain_LIBRARIES} ${OIS_LIBRARIES} Caelum Hydrax)


Далее, для добавления водных эффектов в наше приложение понадобится правильно проинициализированный экземпляр класса Hydrax::Hydrax:
Код
// в файле LandscapeApplication.hpp
class LandscapeApplication : public BaseApplication
{
// ...
protected:
// ...
Hydrax::Hydrax* mHydrax;
};

// в файле LandscapeApplication.cpp
void LandscapeApplication::createScene(void)
{
// ...
mHydrax = new Hydrax::Hydrax(mSceneMgr, mCamera, mWindow->getViewport(0));
Hydrax::Module::ProjectedGrid* mModule = new Hydrax::Module::ProjectedGrid( // модуль проекционной сетки
mHydrax, // указатель на главный класс Hydrax
new Hydrax::Noise::Perlin(/* без особых параметров */), // модуль для создания ряби
Ogre::Plane(Ogre::Vector3(0,1,0), Ogre::Vector3(0,0,0)), // водная поверхность
Hydrax::MaterialManager::NM_VERTEX, // режим карты нормалей
Hydrax::Module::ProjectedGrid::Options(64)); // опции сетки
mHydrax->setModule(mModule);
mHydrax->loadCfg("HydraxDemo.hdx");
mHydrax->create();
}


Запустив наше приложение, наблюдаем неожиданную картину:



Вместо воды какие-то чернила. Что же произошло?
Дело в том, что Caelum подгоняет опции Ogre, связанные с туманом так, чтобы они наиболее соответствовали атмосферной модели, используемой в Caelum. Тем не менее, эти опции не дают воде из Hydrax рендериться правильно. Немного подкрутив опции Caelum (а именно, сказав mCaelumSystem->setGlobalFogDensityMultiplier(0.01), т.е. уменьшив плотность тумана), можно убедиться, что вода, создаваемая Hydrax, на самом деле на месте:



Тщательная подгонка параметров тумана выходит за рамки этой статьи, поэтому в нашем тестовом приложении мы попросту отключим туман:
void LandscapeApplication::createScene(void)
{
// ...
cloudLayer->setHeight(5000);
mCaelumSystem->setManageSceneFog(Ogre::FOG_NONE);

Тут нас поджидает следующая неожиданность:



Такая нелицеприятная картина — результат того, что ландшафт, добавленный нами на третьем шаге, не является обычным объектом (entity) Ogre, поэтому Hydrax не учитывает его при расчёте водной поверхности. Для исправления этой ситуации достаточно добавить технику глубины в материал ландшафта:
void LandscapeApplication::createScene(void)
{
// ...
mHydrax->create();

mHydrax->getMaterialManager()->addDepthTechnique(
mTerrainGroup->getTerrain(0, 0)->getMaterial()->createTechnique());

В результате чего получим следующую радующую глаз картину:



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



Кроме того, расположение солнечного блика на воде не соответствует действительному положению солнца, более того, блик остаётся даже после его захода:



Для решения этой проблемы воспользуемся слегка модифицированным кодом, отвечающим за интеграцию Caelum и Hydrax, из вышеупомянутого редактора Ogitor:
Кот
// в файле LandscapeApplication.hpp
class LandscapeApplication : public BaseApplication
{
// ...
protected:
virtual bool frameEnded(const Ogre::FrameEvent& evt);
Ogre::Vector3 mOriginalWaterColor;

// в файле LandscapeApplication.cpp
void LandscapeApplication::createScene(void)
{
// ...
mHydrax->loadCfg("HydraxDemo.hdx");
mOriginalWaterColor = mHydrax->getWaterColor();
// ...

bool LandscapeApplication::frameEnded(const Ogre::FrameEvent& evt)
{
Vector3 value = mCaelumSystem->getSun()->getSceneNode()->_getDerivedPosition();
ColourValue cval = mCaelumSystem->getSun()->getBodyColour();
mHydrax->setSunPosition(value);
mHydrax->setSunColor(Vector3(cval.r,cval.g,cval.b));
Caelum::LongReal mJulian = mCaelumSystem->getUniversalClock()->getJulianDay();
cval = mCaelumSystem->getSunLightColour(mJulian,
mCaelumSystem->getSunDirection(mJulian));
mHydrax->setWaterColor(Vector3(cval.r - 0.3, cval.g - 0.2, cval.b));
Vector3 col = mHydrax->getWaterColor();
float height = mHydrax->getSunPosition().y / 10.0f;
Hydrax::HydraxComponent c = mHydrax->getComponents();
if(height < 0)
{
if(mHydrax->isComponent(Hydrax::HYDRAX_COMPONENT_CAUSTICS))
mHydrax->setComponents(Hydrax::HydraxComponent(
c ^ Hydrax::HYDRAX_COMPONENT_CAUSTICS));
} else {
if(!mHydrax->isComponent(Hydrax::HYDRAX_COMPONENT_CAUSTICS))
mHydrax->setComponents(Hydrax::HydraxComponent(
c | Hydrax::HYDRAX_COMPONENT_CAUSTICS));
}
if(height < -99.0f)
{
col = mOriginalWaterColor * 0.1f;
height = 9999.0f;
}
else if(height < 1.0f)
{
col = mOriginalWaterColor * (0.1f + (0.009f * (height + 99.0f)));
height = 100.0f / (height + 99.001f);
}
else if(height < 2.0f)
{
col += mOriginalWaterColor;
col /= 2.0f;
float percent = (height - 1.0f);
col = (col * percent) + (mOriginalWaterColor * (1.0f - percent));
}
else
{
col += mOriginalWaterColor;
col /= 2.0f;
}
mHydrax->setWaterColor(col);
mHydrax->setSunArea(height);
mHydrax->update(evt.timeSinceLastFrame);
return true;
}


В результате получаем романтический закат и… пару новых артефактов:



Во-первых, при движении камерой на горизонте то появляются, то исчезают длинные белые полоски. Происходит это потому, что водная поверхность пересекается с дальней плоскостью отсечения (far clipping plane) на достаточно близком расстоянии, что и порождает видимые артефакты. Решение этой проблемы — проще некуда, достаточно увеличить расстояние до этой плоскости:
mCamera->setFarClipDistance(1000000);

Во-вторых, нижняя часть солнца, «ушедшая» под воду, которую вообще не должно быть видно, имеет бросающиеся в глаза белые края. Корень этой проблемы такой же, как и у проблемы с ландшафтом: солнце (и луна) не являются обычными объектами Ogre; для её исправления также нужно добавить технику глубины в материал этих объектов. Наилучшее место для этого — файлы Sun.material и moon.material в каталоге media/Caelum; необходимо в каждый материал, описанный в этих файлах, добавить следующий абзац:
technique HydraxDepth
{
scheme HydraxDepth
pass
{
lighting off
texture_unit
{
colour_op_ex modulate src_manual src_current 0 0 0
}
}
}

Архив media_fixed.zip со всеми необходимыми исправлениями можно скачать здесь. После запуска приложения с ними всё встаёт на свои места:



Шаг пятый: растительность


Последним штрихом добавим немного флоры в наш уютный пейзаж. Для этого как нельзя лучше подходит библиотека Ogre Paged Geometry Engine, ибо она позволяет рендерить огромные количества небольших мешей на больших расстояниях, что особенно ценно для построения, например, сцены леса с деревьями, кустами, травой, камнями, и прочая, и прочая. Распространяется эта библиотека по одной из самых либеральных среди свободных лицензии — MIT (как и сам Ogre).
Последнюю версию Paged Geometry можно скачать этому адресу, опять-таки все необходимые файлы я скопировал в дерево исходников тестового проекта. Библиотека поддерживает CMake, поэтому в наш CMakeLists.txt понадобится добавить всего несколько строк: расположение заголовочных файлов, расположение библиотечного файла и расположение самих исходников библиотеки:
include_directories(
# ...
"PagedGeometry/include"
"${CMAKE_BINARY_DIR}/PagedGeometry/include"

# ...
add_subdirectory(PagedGeometry)
# ...
target_link_libraries(LandscapeApp ${OGRE_LIBRARIES} ${OGRE_Terrain_LIBRARIES} ${OIS_LIBRARIES}
Caelum Hydrax PagedGeometry)

Далее, в нашем приложении мы ограничимся добавлением трёх типов объектов:
  1. травы;
  2. деревьев;
  3. прочей растительности (кустов, цветов и грибов).

Для этого добавим указатели на экземпляры ключевого класса Forests::PagedGeometry (и вспомогательного Forests::GrassLoader) в наш главный класс, не забыв об необходимых для этого подключаемых файлах:
#include <PagedGeometry.h>
#include <GrassLoader.h>

class LandscapeApplication : public BaseApplication
{
// ...
protected:
// ...
Forests::PagedGeometry *grass, *trees, *bushes;
Forests::GrassLoader* grassLoader;
};

Затем проинициализируем эти указатели нулями, изменив конструктор LandscapeApplication следующим образом:
LandscapeApplication::LandscapeApplication(void) : grass(NULL), trees(NULL), bushes(NULL)
{
}

Кроме того, нам понадобится функция, возвращающая высоту ландшафта в точке с заданными координатами x и z; вызывая эту функцию, Paged Geometry будет правильным образом расставлять всю свою бутафорию на земле. Нужна будет именно функция, а не метод класса, т.к. в методы класса неявно передаётся указатель this, который библиотеке будет попросту неоткуда взять. Следовательно, нам понадобится глобальная переменная — указатель на класс, управляющий группой страниц ландшафта, для связи между собственно ландшафтом и нашей функцией. Что ж, реализуем задуманное:
static TerrainGroup* terrainGroup = NULL;

static float getTerrainHeight(float x, float z, void* userData)
{
OgreAssert(terrainGroup, "Terrain isn't initialized");
return terrainGroup->getHeightAtWorldPosition(x, 0, z);
}

Так как данная переменная изначально равна NULL, её необходимо проинициализировать указателем на экземпляр класса TerrainGroup, что мы и сделаем следующей строчкой после кода инициализации ландшафта в методе createScene:
terrainGroup = mTerrainGroup;

Наконец, теперь можно добавить код инициализации всех необходимых нам объектов в метод createScene:
Код
// трава
grass = new PagedGeometry(mCamera);
grass->addDetailLevel<GrassPage>(160); // уровень детализации: не рендерить траву дальше 60 единиц от камеры
grassLoader = new GrassLoader(grass);
grass->setPageLoader(grassLoader);
grassLoader->setHeightFunction(getTerrainHeight); // функция, возвращающая высоту ландшафта в заданной точке
GrassLayer* l = grassLoader->addLayer("3D-Diggers/plant1sprite"); // добавить слой травы
l->setMinimumSize(0.9f, 0.9f); // максимальный...
l->setMaximumSize(2, 2); // ... и минимальный размер
l->setAnimationEnabled(true); // включить анимацию
l->setSwayDistribution(7.0f); // колебания травы от ветра
l->setSwayLength(0.1f); // амплитуда колебаний - 0.1 единиц
l->setSwaySpeed(0.4f); // скорость колебаний
l->setDensity(3.0f); // плотность травы
l->setRenderTechnique(GRASSTECH_SPRITE); // рендерим с помощью спрайтов
l->setFadeTechnique(FADETECH_GROW); // при движении камеры трава должна медленно подниматься
l->setColorMap("terrain_texture2.jpg"); // карта распределения цвета
l->setDensityMap("densitymap.png"); // карта плотности
l->setMapBounds(TBounds(0, 0, 3000, 3000)); // границы слоя
// деревья
trees = new PagedGeometry(mCamera);
trees->addDetailLevel<WindBatchPage>(150, 30); // использовать батчинг на расстоянии между 150 и 180 единицами
trees->addDetailLevel<ImpostorPage>(900, 50); // заменять модели спрайтами на расстоянии между 900 и 950 единицами
TreeLoader2D *treeLoader = new TreeLoader2D(trees, TBounds(0, 0, 5000, 5000));
trees->setPageLoader(treeLoader);
treeLoader->setHeightFunction(getTerrainHeight); // функция, возвращающая высоту ландшафта в заданной точке
treeLoader->setColorMap("terrain_lightmap.jpg"); // карта распределения цвета
Entity *tree1 = mSceneMgr->createEntity("Tree1", "fir05_30.mesh"); // загрузить модели деревьев
Entity *tree2 = mSceneMgr->createEntity("Tree2", "fir14_25.mesh");
trees->setCustomParam(tree1->getName(), "windFactorX", 15); // параметры ветра
trees->setCustomParam(tree1->getName(), "windFactorY", 0.01f);
trees->setCustomParam(tree2->getName(), "windFactorX", 22);
trees->setCustomParam(tree2->getName(), "windFactorY", 0.013f);
// распределяем случайным образом 5000 копий деревьев
Vector3 position = Vector3::ZERO;
Radian yaw;
Real scale;
for (int i = 0; i < 5000; i++)
{
yaw = Degree(Math::RangeRandom(0, 360));
position.x = Math::RangeRandom(0, 2000); // координата Y не требуется, т.к. будет вычислена
position.z = Math::RangeRandom(2300, 4000); // с помощью ф-ии getTerrainHeight, для быстродействия
scale = Math::RangeRandom(0.07f, 0.12f);
if (Math::UnitRandom() < 0.5f)
{
if (Math::UnitRandom() < 0.5f)
treeLoader->addTree(tree1, position, yaw, scale);
}
else
treeLoader->addTree(tree2, position, yaw, scale);
}
// кусты/грибы
bushes = new PagedGeometry(mCamera);
bushes->addDetailLevel<WindBatchPage>(80, 50);
TreeLoader2D *bushLoader = new TreeLoader2D(bushes, TBounds(0, 0, 5000, 5000));
bushes->setPageLoader(bushLoader);
bushLoader->setHeightFunction(getTerrainHeight);
bushLoader->setColorMap("terrain_lightmap.jpg");
Entity *fern = mSceneMgr->createEntity("Fern", "farn1.mesh"); // загрузить модель папоротника
Entity *plant = mSceneMgr->createEntity("Plant", "plant2.mesh"); // загрузить модель цветка
Entity *mushroom = mSceneMgr->createEntity("Mushroom", "shroom1_1.mesh"); // загрузить модель гриба
bushes->setCustomParam(fern->getName(), "factorX", 1); // параметры ветра
bushes->setCustomParam(fern->getName(), "factorY", 0.01f);
bushes->setCustomParam(plant->getName(), "factorX", 0.6f);
bushes->setCustomParam(plant->getName(), "factorY", 0.02f);
// распределяем случайным образом 20000 копий кустов и грибов
for (int i = 0; i < 20000; i++)
{
yaw = Degree(Math::RangeRandom(0, 360));
position.x = Math::RangeRandom(0, 2000);
position.z = Math::RangeRandom(2300, 4000);
if (Math::UnitRandom() < 0.8f) {
scale = Math::RangeRandom(0.3f, 0.4f);
bushLoader->addTree(fern, position, yaw, scale);
} else if (Math::UnitRandom() < 0.9) {
scale = Math::RangeRandom(0.2f, 0.6f);
bushLoader->addTree(mushroom, position, yaw, scale);
} else {
scale = Math::RangeRandom(0.3f, 0.5f);
bushLoader->addTree(plant, position, yaw, scale);
}
}


… и код финализации в деструктор LandscapeApplication:
Код финализации
LandscapeApplication::~LandscapeApplication(void)
{
if(grass)
{
delete grass->getPageLoader();
delete grass;
grass = NULL;
}
if(trees)
{
delete trees->getPageLoader();
delete trees;
trees = NULL;
}
if(bushes)
{
delete bushes->getPageLoader();
delete bushes;
bushes = NULL;
}
mSceneMgr->destroyEntity("Tree1");
mSceneMgr->destroyEntity("Tree2");
mSceneMgr->destroyEntity("Fern");
mSceneMgr->destroyEntity("Plant");
mSceneMgr->destroyEntity("Mushroom");
}


(без этого кода приложение будет падать с ошибкой при выходе).
Ну и самое главное — у экземпляров класса Forests::PagedGeometry необходимо каждый фрейм вызывать метод update() для того, чтобы наш лес рендерился, плавно покачиваясь под порывами виртуального ветра:
bool LandscapeApplication::frameEnded(const Ogre::FrameEvent& evt)
{
// ...
grass->update();
trees->update();
bushes->update();
return true;
}

Запустив приложение, мы можем наблюдать пасторальный лесной пейзаж:





… благо, никаких подводных камней нас здесь не поджидает. Единственное, на что стоит обратить внимание — падение производительности: несмотря на множество техник для повышения быстродействия (начиная от банального LOD и заканчивая механизмом разделения на страницы, подобным аналогичному в подсистеме ландшафта и подменой настоящих деревьев на спрайты на больших расстояниях), Paged Geometry всё-таки требует аккуратной настройки количества отображаемых экземпляров растительности и качества картинки. Здесь опять-таки можно порекомендовать использовать редактор Ogitor, который имеет поддержку Paged Geometry, позволяя легко набросать в нужных местах растительность (примерно как мазать кистью в Paint) и настроить все необходимые параметры.

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

В благодарность всем, кто дочитал до этого места — ещё пара симпатичных скриншотов:





И видео (его делал не я, но оно создано с использованием рассматриваемых библиотек — Ogre, Hydrax и Caelum):



Также хочу выразить благодарность хабраюзеру Сергею ViruScD за помощь в подготовке статьи, Андрею engine9 за помощь в работе с движком Ogre и создателям сайта dumpz.org за великолепную подсветку синтаксиса.

UPD: Переснял скриншоты со включённым анти-алиасингом, спасибо m1el за замечание.
Tags:ogre3dландшафткомпьютерная графика
Hubs: Game development
Rating +65
Views 16k Add to bookmarks 83
Comments
Comments 35

Popular right now

Game-дизайнер
March 10, 2021128,400 ₽GeekBrains
Data Scientist
March 9, 2021126,000 ₽Нетология