Добавляем поддержку вращения объектов сцены с помощью кватернионов в Irrlicht engine.

В этой статье я расскажу как добавить поддержку кватернионов в графический движок на примере движка Irrlicht. Код можно легко перенести в любой другой движок с открытым кодом. Подробнее узнать о кватернионах

можно Кватернионы в википедии и Статья о кватернионах Gamedev.ru.

Автор: Сергей Тарабан

:ad:

Зачем нужны вращения на кватернионах в Irrlicht.

Irrlicht – простой и удобный открытый игровой графический движок. Однако он до сих пор не имеет поддержки трансформаций вращения на кватернионах. Трансформацию для node сцены можно задать только в углах Эйлера. Это приводит к таким известным проблемам как:
  • Gimbal lock или Складыванию рамок.
  • Ошибкам трансформаций вращения при импорте моделей и сцены из графических 3D редакторов.
  • Имплементации готового кода на кватернионах из других движков.
Про Gimbal lock сказано очень много. Применительно к компьютерной графике это явления означает то, что матрицы вращения не являются полностью независимыми и вы не можете использовать их при задании последовательности независимых вращений. Это значит, например, что если вы повернете объект последовательно вокруг оси X, оси Y и, затем, оси Z, вы получите совсем не ту трансформацию, которую ожидали. Это явление также называют «Потерей степени свободы при использовании углов Эйлера». В результате вы получите поворот вокруг оси Х на угол равный сумме углов поворота вокруг оси X и оси Z.

В частности это хорошо показано этими выражением: Gimbal lock proof Gimbal lock proof

Еще один неприятный сюрприз ожидает вас, если вы решили импортировать сцену из графического редактора, например, Autodesk 3ds Max. Max хранит трансформации в кватернионах и при загрузки в Irrlicht вам надо преобразовывать их в углы Эйлера. В результате некоторые объекты на карте будут повернуты очень «странно» или вообще неверно или из-за уже упомянутого Gimbal lock, или из-за алгоритма функции преобразования кватернионов в углы Эйлера, ведь может получить разные варианты и порядок поворота для одного и того же кватерниона.

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

Практически все хорошие 3D редакторы и движки хранят трансформации вращения всех своих объектов в виде кватернионов. Я надеялся, что поддержку кватернионов введут в релизе Irrlicht 1.8, но увы все осталось как есть. В своем движке на основе Irrlicht я уже давно сделал эту имплементацию.

Примеры Gimbal lock "из жизни".

Рассмотрим тестовую сцену в 3ds max в виде такого вот дерева. Каждая ветка дерева - это один и тот же конус, который имеет разную трансформацию вращения, масштаба и позиции. Попробуем загрузить эту сцену в Irrlicht. Для экспорта из max я использую свой скрипт. А для загрузки в своем движке просто парсю файл сцены, где указаны трансформации для каждого из объектов.

На рисунке ниже скрин из 3ds max - это как должно быть. 3dsmax_test_object

ну а теперь как это загрузится в Irrlicht без поддержки кватернионов

test_object_irr_no_quaternions

А вот пример сцены когда трансформации задаются кватернионами:

test_object_irr_quaternions_supported

Как видите в таком случае cцена отображается так же как и в 3ds max.

Приступаем к реализации.

В библиотеке движка есть неплохой класс irr::core::Quaternion, который поддерживает все необходимые операция с кватернионами. Нам нужно только добавить новые методы для интерфейса ISceneNode и для каждой из имплементаций графического драйвера.

В этой статье я покажу как добавить поддержку кватернионов для OGLESDriver (OpenGL ES 1.0) и OGLES20Driver (OpenGL ES 2.0). Для остальных драйверов будет практически копипаст, зависит от того, как у вас реализована трансформация вершин – через фиксированный конвейер или в шейдере. Обе этой реализации я опишу на примере эти двух драйверов.

Превое что мы делаем, это добавляем приватное поле Quaternion RelativeRotationQ и методы setRotationQ() и getRotationQ() в класс ISceneNode для того, чтобы объекты сцены могли трансформироваться использую кватернион. core::quaternion RelativeRotationQ;

virtual void setRotationQ(const core::quaternion& rotation) { RelativeRotationQ = rotation; }

virtual const core::quaternion& getRotationQ() const { return RelativeRotationQ; }

В интерфейс IVideoDriver добавлем новый метод setTransformQ virtual void const setTransformQ( core::quaternion q ) = 0;

И его реализацию в класс базового драйвера CNullDriver const void CNullDriver::setTransformQ( core::quaternion q ) { RotationTransformQuat = rot; }

А также новое поле core::quaternion RotationTransformQuat;

Далее во всех реализациях интерфейса ISceneNode в функцию render() перед вызовом функции setTransfor добавляем код: driver->setTransformQ(RelativeRotationQ);

например как в функции CMeshSceneNode::render() … driver->setTransformQ(RelativeRotationQ); driver->setTransform(video::ETS_WORLD, AbsoluteTransformation); …

Теперь переходим к реализации драйвера. Для OpenGL ES 1.0 и аналогично в OpenGL для FFP (фиксированный конвейер) просто добавляем код в функцию setTransform

case ETS_WORLD: { glMatrixMode(GL_MODELVIEW); glLoadMatrixf(Matrices[ETS_VIEW].pointer()); glMultMatrixf(Matrices[ETS_WORLD].pointer());

//quaternion rotation support float angle = 0.0f; core::vector3f axis; RelativeRotationQ.toAngleAxis(angle, axis); glRotatef(angle*core::RADTODEG, axis.X, axis.Y, axis.Z); } Для увеличения производительности можно вынести преобразование кватерниона в другое место. Этот код приведен для наглядности.

Для OpenGL ES 2.0 добавить поддержку будет несколько сложнее. Для начала нам надо модифицировать вершинный GLSL шейдер COGLES2FixedPipeline.vsh чтобы он поддерживал поворот вершин кватернионом. Добавим туда новый юниформ fvQuatRotation. Через него мы будем передавать кватернион в шейдер.

uniform vec4 fvQuatRotation;

Теперь добавим функции перемножения кватерниона с вектором:

Я привожу два варианта алгоритма quaternion multiplication для GLSL шейдеров – один без оптимизации, второй – с оптимизацией. Неоптимизированный алгоритма «в лоб по формуле»: vec4 quatMultyQV( vec4 q, vec4 v ) { return vec4(quatMultyQQ(q, quatMultyQQ(vec4(v.xyz, 0.), getConjugate(q))).xyz, v.w); }

vec4 quatMultyQQ( vec4 q, vec4 rq ) { return vec4( cross(q.xyz, rq.xyz) + q.wrq.xyz + rq.wq.xyz, q.w*rq.w - dot(q.xyz, rq.xyz) ); }

vec4 getConjugate( vec4 q ) { return vec4(-q.x, -q.y, -q.z, q.w ); }

Оптимизированный в-нт: vec4 quatMultyQV( vec4 q, vec4 v ) //q- кватернион, v – вектор на который умножаем { vec3 x = q.xyz; float w = q.w; vec3 a = cross(x,v.xyz) + w*v.xyz; return vec4((cross(a,-x) + dot(x,v.xyz)x + wa), v.w); }

Оба варианта рабочие.

Далее напишем код трансформации вершин: // inVertexPosition - attribute vec4 vec4 inRotVertexPosition = quatMultyQV( fvQuatRotation, inVertexPosition); vec4 fvObjectPosition = matWorld * inRotVertexPosition; gl_Position = matViewProjection * fvObjectPosition;

Теперь вернемся к коду движка. Добавляем передачу значения трансформации в функции COGLES2FixedPipelineShader::OnRender()

s32 unifQuatRotationLocation = this->getUniformLocation(“fvQuatRotation”); core::quaternion quat = Driver->getTransformQuat(); float vec[4] = {quat.X, quat.Y, quat.Z, quat.W}; glUniform4fv(unifQuatRotationLocation, 1, reinterpret_cast<glfloat*>(&(vec)));

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

И последнее. Имейте в виду, что в разных графических редакторах может использоваться разная система координат. Обычно или правосторонняя, или левосторонняя. Этот код позволит преобразовать кватернион из правостороннего XYZ базиса в левосторонний XZY. Я использую эту функцию для конвертации кватернионов из 3ds max. Как видите кватернионы преобразуются довольнго просто, вам надо просто инвертировать нужные оси.

quaternion convertFromXYZToLeftHandedXZY( quaternion qsrc ) { quaternion q; float angle = 0; core::vector3df axis; qsrc->toAngleAxis(angle, axis); core::swap(axis.Z, axis.Y); axis.Z = -axis.Z; axis.X = -axis.X; axis.Y = -axis.Y; q.makeIdentity(); q.fromAngleAxis(angle, axis); return q; }

Автор: Сергей Тарабан