Задача интеграции Irrlicht в какую нибуть GUI в интренете обсуждалось много и довольно подробно. Например есть классы для связки Irrlicht и wxWidgets, есть наработки для Python, есть и готовые классы для Qt. Но они давно не обновлялись, и многое что не работает, а помощь на англоязычном форуме Irrlicht мало чего дала, когда я начинал разбирался с этим движком.

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

Для чего нужно интегрировать игровой движок в Gui оболочку спросите вы ? Ну если вы пишите непосредственно игру, то да, смысла в этом нету, однако если вы разрабатываете рендеренг какой нибудь сцены то скорее всего понадобятся разные кнопки, скролл бары, деревья и т.д. Конечно можно использовать уже реализованные в движке классы irr::gui, но они не так удобны и не так продвинуты как Qt. Плюс давно пишу на этой библиотеки и полностью доволен :)

Начнем по порядку. Можно выделить две особенности интеграции:

  1. Выделяем отдельный поток для рендеринга сцен(много поточное приложение). То в теории код отличаться сильно не будет, например от примеров Irrlict'а. Я это пока не реализовывал, но в скором времени прпробую.
  2. Пишем однопоточное приложение. Тут будут пара грабель, и интересных особенностей.

Назовем наш класс Terrain3D и унаследуемся от QWidget:

// Конструктор

class Terrain3D : public QWidget
{
Q_OBJECT
public:

/*! \brief Конструктор нашего класса. Можно задать парента. Используем отрисовку через OpenGL по умолчанию
*/
Terrain3D(QWidget *parent, irr::video::E_DRIVER_TYPE driverType = irr::video::EDT_OPENGL/*EDT_BURNINGSVIDEO*/);

/*! \brief Деструктор

*/

virtual ~Terrain3D();

...

...

...

};

Нам понадобятся переопределить пару функций, это для отрисовки, ресайза, событий клавиатуры и мышки.

protected:
virtual QPaintEngine * paintEngine() const;
virtual void resizeEvent(QResizeEvent *event);
virtual void paintEvent(QPaintEvent *event);

Реализация конструктора класса:

Terrain3D::Terrain3D(QWidget *parent, irr::video::E_DRIVER_TYPE driverType) :
QWidget(parent)
{
setAttribute(Qt::WA_PaintOnScreen);
setAttribute(Qt::WA_NoBackground);
setAttribute(Qt::WA_NoSystemBackground);
setFocusPolicy(Qt::StrongFocus);
setAutoFillBackground(false); // Говорим Qt чтобы не обновлял фон автоматом, этим будет заниматься Irrlicht

...

...

m_IrrDevice = 0;// По умолчанию 0, для того чтобы при инициализации не произошло создания более одного устройства

};

Флаги которые мы устанавливаем, необходимы для того, чтобы отрисовкой полностью занимался Irrlicht, если не устанавливать их то будет мерцание изображния, тоесть в paintEvent() Irrlicht отрисует нам картинку, а Qt после этого её сотрет, а нам этого не нужно.

Теперь нам нужна функция инициализации, в которой будет происходить создание движка Irrlicht. Тут главная особенность в том, что мы используем QWidget::winId(), которая возвращает системный индификатор окна, и вызывать её нужно в определенном порядке, если окно не будет отображено, то мы получим неправильный индефикатор. Или если мы не включим некоторые флаги при создании движка, то тоже не получим картинку. Если в консоль после создания движка высыпались сообщения следующего содержания, ты мы иеем дело именно с этим:

 

Linux 2.6.35-28-generic-pae #49-Ubuntu SMP Tue Mar 1 14:58:06 UTC 2011 i686

Creating X window...

Visual chosen: : 39

X Error: BadMatch (invalid parameter attributes)

From call : unknown

X Error: GLXBadDrawable

From call : unknown

Could not make context current.

Using renderer: OpenGL

OpenGL driver version is not 1.2 or better.

Warning: OpenGL device only has one texture unit. Disabling multitexturing.

GLSL not available.

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

terrain3D = new Terrain3D(this);
setCentralWidget(terrain3D);
show();
terrain3D->initialize();

Посмотрим реализацию initialize().

void Terrain3D::initialize()
{
if(m_IrrDevice != 0) // Чтобы не инициализировать дважды
return;

// Создаем Irrlicht устойство в первый раз
irr::SIrrlichtCreationParameters createParams;
createParams.DriverType = m_DriverType;
createParams.WindowSize = irr::core::dimension2d<irr::u32>(size().width(), size().height());
createParams.Bits = 32;
createParams.Stencilbuffer = true; // Устанавливаем для избежания ошибки при инициализации
createParams.IgnoreInput = false;
createParams.Fullscreen = false;
// createParams.AntiAlias = 2; // Лучше не указывать, у меня если установлено сглаживание, получаю или сегфолт, или не получаю картинки
createParams.Doublebuffer = true; // Устанавливаем для избежания ошибки при инициализации
createParams.Vsync = false; // Включите если нужно ограничить фпс
createParams.Stereobuffer = false;

// Id окна
createParams.WindowId = (void*)winId();

// Создаем движок
if((m_IrrDevice = irr::createDeviceEx(createParams)) == 0)
qDebug() << "failed to create Irrlicht device";
m_IrrDevice->setResizable(true);

}

Теперь перейдем непоследственно к отрисовки изображения. Если вы смотьрели примеры, то могли видеть, что вся отрисовка выполняется в цикле while(device->run()), поэтому я и говорил, что если приложение много поточное, то расхождение с примерами будет минимальным. У нас же другая ситуация, мы должны рисовать в paintEvent(). Ч то мы и сделаем. Давайте сначало напищем функцию для открытия данных, а потом перейдем к реализации paintEvent(). В этой функции сосздадим фпс камеру, поверхность, небо и коллизию.

void Terrain3D::openMap3D(const QString & filename)
{

// Все что нужно для связи с ядром Irrlicht
video::IVideoDriver* driver = m_IrrDevice->getVideoDriver();
scene::ISceneManager* smgr = m_IrrDevice->getSceneManager();
gui::IGUIEnvironment* env = m_IrrDevice->getGUIEnvironment();

driver->setTextureCreationFlag(video::ETCF_ALWAYS_32_BIT, true);

m_Camera = smgr->addCameraSceneNodeFPS(smgr->getRootSceneNode()); // делаем камеру активной
m_Camera->setFarValue(10000);
m_Camera->setNearValue(0.1f);
m_Camera->setFOV(60.0f * irr::core::DEGTORAD);

// Создаем поверхность
m_Terrain = smgr->addTerrainSceneNode("../../media/terrain-heightmap-1.bmp", // Скармливаем карту вывсот
smgr->getRootSceneNode(), // Parent
-1, // Id
vector3df(0.0f, 0.0f, 0.0f), // Позиция
vector3df(0.0f, 0.0f, 0.0f), // Вращение
vector3df(1.0f, 1.0f, 1.0f), // Масштаб
video::SColor(255,255,255,255),
5,
ETPS_17,
4,
true);
m_Terrain->setScale(scale);
m_Terrain->setMaterialType(irr::video::EMT_DETAIL_MAP);
m_Terrain->setMaterialFlag(irr::video::EMF_NORMALIZE_NORMALS, true);

// Создаем небо
driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, false);
m_Skydome = smgr->addSkyDomeSceneNode(driver->getTexture("../../media/sky/skydome.jpg"), 16, 8, 0.95f, 2.0f);
driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, true);

// Создаем коллизию на карту
m_Selector = smgr->createTerrainTriangleSelector(m_Terrain, 0);
m_Terrain->setTriangleSelector(m_Selector);

m_Animator = smgr->createCollisionResponseAnimator(m_Selector,
m_Camera,
core::vector3df(1, 0.1, 1),
core::vector3df(0, 0, 0),
core::vector3df(0, 0, 0));
m_Selector->drop();
m_Camera->addAnimator(m_Animator);
m_Animator->drop();

}

Переходим к отрисовке. Сначало сделаем просто отрисовку. я расскажу о её недостадках, а как их обойти расскажу в следующей статье.

void Terrain3D::paintEvent(QPaintEvent */*event*/)

{

m_IrrDevice->getVideoDriver()->beginScene(true, true, m_ClearColor);

m_IrrDevice->getSceneManager()->drawAll();

m_IrrDevice->getGUIEnvironment()->drawAll();

m_IrrDevice->getVideoDriver()->endScene();

}

Для того чтобы все правильно отрисовывалось при изменении размера окна, нужно переопредилить resizeEvent и написать функцию setAspectRaitio

void Terrain3D::resizeEvent(QResizeEvent *event)
{
if( m_IrrDevice == 0 )
return;

int newWidth = event->size().width();
int newHeight = event->size().height();

m_IrrDevice->getVideoDriver()->setViewPort(core::rect<s32>(vector2d<s32>(0, 0), dimension2d<irr::u32>(newWidth, newHeight)));
m_IrrDevice->getVideoDriver()->OnResize(irr::core::dimension2d<irr::u32>(newWidth, newHeight));

setAspectRatio();
QWidget::resizeEvent(event);
}

void Terrain3D::setAspectRatio(irr::f32 aspect)
{
if(m_IrrDevice == 0)
return;

irr::f32 newAspectRatio;
if(aspect < 0) // Автоматом
{
irr::core::dimension2d<irr::u32> screenSize = m_IrrDevice->getVideoDriver()->getScreenSize();
newAspectRatio = (irr::f32)screenSize.Width / (irr::f32)screenSize.Height;
}
else
newAspectRatio = aspect;

// Устанавливаем соотношение сторон для активной камеры
irr::scene::ISceneManager * mgr = getSceneManager(); // Получаем менеджер сцены
if(mgr->getActiveCamera())
mgr->getActiveCamera()->setAspectRatio(newAspectRatio); // Устанавливаем соотношение
}

Все, теперь у нас все должно рисоваться. Теперь о граблях:

  1. При такой отрисвке, нам необходимо самим вызывать функцию QWidget::update() чтобы обновить сцену.
  2. Камера не будет реагировать на нажатие кнопок клавиатуры и мыши.

Об устранении этих недостатков будет написано в следующей статье.