WWW.ROSSPROGRAMMPRODUCT.COM - Главная страница сайта

Статьи для программистов. Переводы.



Euler Angles are Evil.

By Steve Baker

Вольный перевод с английского. Публикуется с разрешения и одобрения автора.
Оригинал этой статьи вы можете найти на собственном сайте Стива Бейкера www.sjbaker.org по адресу: http://sjbaker.org/steve/omniv/eulers_are_evil.html

Использование углов Эйлера может завести в тупик.

Автор Стив Бейкер
Автор перевода - Вараксин А. Г.

Введение

Большинство программистов, впервые пытающихся запрограммировать перемещение камеры, пишут примерно такой код:

  while ( 1 )
  {
    ...считываем изменения углов поворота рукоятки джойстика или другого устройства ввода ;
    ...складываем полученные изменения с текущими углами ;
    ...конвертируем текущие углы в матрицу ;
    ...инвертируем матрицу ;
    ...помещаем матрицу на верх стека GL_MODELVIEW ;

    ...рисуем сцену ;
  }

Этот код не будет работать, или, по крайней мере, будет казаться, что код работает, но только для первых изменений и, может быть для маленьких изменений в тангаже (pitch) и крене (roll). Вы только что обнаружили это - и вот почему вы читаете эту статью - верно?

Почему этот код не работает?

Итак, когда вы конвертируете эти три угла в матрицу (или когда вы используете три функции glRotate), программа, формирующая матрицу, делает какие-то предположения о ПОРЯДКЕ этих трёх вращений.

Вообразите самолёт. Три возможные вращения по осям, называются 'Рысканье' ('Heading'), 'Тангаж' ('Pitch') и 'Крен' ('Roll'). 'Heading' - 'Рысканье' иногда называют 'Yaw' - но я ненавижу этот выбор, поскольку в укороченном виде это слово записывают как 'Y', а это может привести к путанице с обозначением оси ординат Y - поэтому я и заменил слово 'Yaw' на 'Heading'.

  • Изменения в 'Heading' (Рысканье) производят повороты налево или направо.
  • Изменения в тангаже ('Pitch') заставляют нос самолёта опускаться, а хвост подниматься (или наоборот).
  • Изменения в крене ('Roll') поднимают конец одного крыла и опускают конец другого.

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

(Когда вы используете три угла для представления вращений, их часто называют как "Эйлеровы углы вращения" или просто 'Eulers'. Я же обычно записываю Эйлеровы углы как (H,P,R).)

Фактически, вся проблема состоит в том, что когда вы комбинируете эти три вращения, вы ДОЛЖНЫ делать это в особом порядке - вы можете выбрать любой порядок - но математика всегда вынуждает вас сделать конкретный выбор. То есть вы можете совершить повороты в таком порядке: сначала крен, затем тангаж и в конце рысканье. Теперь посмотрим, что может случиться с такой последовательностью операций - и, для простоты, рассмотрим только крен и тангаж.

(Если вы - правша, то вам гораздо легче будет следить за ходом обсуждения, если вы сложите самолётик из бумаги. Сложите один сейчас же! Левши обычно имеют лучшее пространственное воображение и могут во всём разобраться мысленно :-)

  1. Самолёт (при отсутствии вращений) горизонтально выровнен и направлен носом прямо на Север.
  2. Зададим углы Рысканья/Тангажа/Крена - (0,90,90) и посмотрим, что случится:
  3. Самолёт теперь имеет крен в 90 градусов (так , что правый конец крыла указывает на небо, а левый на землю).
  4. ...и тангаж в 90 градусов (имеется ввиду, что нос поднят ВВЕРХ) ...так, что нос самолёта указывает на Запад, а хвост направлен на Восток, правое крыло по-прежнему направлено вверх, и левое - вниз.
  5. Теперь пилот пытается выровнять самолёт по горизонтали, поворачивая рукоятку джойстика направо, - что в соответствии с вышеприведённым кодом должно постепенно уменьшить крен самолёта до нуля. Вы можете ожидать в результате, что нос самолёта будет по-прежнему направлен к Западу, - но на самом-то деле теперь нос самолёта и крен...
  6. Так, в конце этого маневра, HPR - углы изменятся от (0,90,90) до (0,90,0) - каково теперь на самом деле положение самолёта в пространстве?
  7. Давайте вернёмся назад, к первому пункту, (самолёт горизонтально выровнен и направлен носом к Северу) и определим положение самолёта, если HPR - углы заданы, как (0,90,0).
  8. Отлично, нулевой крен задаёт правильное положение крыльев - левое крыло направлено к западу и правое к востоку... и 90 - градусный тангаж поднимает нос самолёта к небу!

Таким образом, после уменьшения угла крена до нуля, наш самолёт, вместо того, чтобы быть горизонтально выровненным и лететь на запад, оказывается в положении с носом, задранным вертикально вверх! С точки зрения пилота, его самолёт имел 90-градусный левый крен и летел на запад, а после поворота рукоятки джойстика направо, с целью выровнять самолёт горизонтально, фактически изменился угол Рысканья, потому, что из положения полёта на запад, самолёт оказался с торчащим вверх носом, поскольку нос самолёта повернулся направо, а хвост - налево, т. е., по определению, совершил "Рысканье". Попытка выровнять самолёт, всё ещё летящий на запад, изменяя угол Рысканья (работая рулем) или Тангаж, также завершаются неудачей!

Такого рода явления часто называют "шарнирный замок" ('gimbal lock') - по имени эффекта, который случается с реальными механизмами, имеющими три осевых шарнира (т. е. с тремя степенями свободы). Когда оси двух шарниров оказываются параллельными друг другу, вы теряете одну степень свободы в этой системе. У меня есть автомобильный ключ зажигания, такой гибкий, что его можно согнуть в прямой угол, но, несмотря на это им легко можно запустить двигатель.

Добавьте третий угол вращения, и дело может зайти в тупик.

Как же нам разрешить эту проблему?

Вы только узнали обо всём этом... Но что же нужно сделать, чтобы это исправить?

Нам необходимо понять, на каком шаге мы сделали ошибку. Проблема в том, что первое изменение угла крена случилось ДО изменения угла тангажа, а уменьшение крена - "выравнивание" - после положения, возникшего в результате первоначальных изменеий углов Крена + Тангажа (т. е. не просто восстановление начального угла крена, что фактически делает наивный код в начале этого документа).

Прекрасно, после сказанного соблазнительно изменить наш код так:

  ...конвертируем первоначальную позицию в матрицу 'position' ;

  while ( 1 )
  {
    ...считываем изменения углов поворота рукоятки джойстика или другого устройства ввода ;
    ...конвертируем изменения углов  в матрицу 'velocity' ;
    ...перемножаем матрицы 'position' и 'velocity' ;
    ...инвертируем полученную матрицу ;
    ...помещаем эту матрицу на верх стека GL_MODELVIEW ;

    ...рисуем сцену ;
  }

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

Всё же имеется раздражающая проблема, возникающая из-за особенностей машинной математики - ошибки округления.

Когда вы сохраняете вращения в качестве Эйлеровых углов, может произойти накопление крошечных ошибок, появляющихся при округлении, - если вы изменяете угол "Рысканья" по 0.1 градуса в каждом из 3600 кадров, то в результате угол "Рысканья" не будет равен точно нулю. Однако, игрок управляющий сценой при помощи джойстика, не заметит этой ошибки - это вполне естественно для людей.

К несчастью матрицы 3х3 и 4х4 могут совершить гораздо больше действий, чем простое вращение. При правильном выборе значений элементов матрицы, она может произвести сдвиг, растяжение или ещё что-нибудь. Вот что случается, когда вы многократно перемножаете матрицы вращения: матрица постепенно начинает производить сдвиг и растяжение - после десяти минут работы джойстиком, вы начнёте невооружённым глазом видеть искажения в сцене. Поскольку пользователь не имеет контроля над кодом, то он и не имеет способа откорректировать искажения.

Существует механизм "исправления" матрицы. Если вы читали мою статью "Подружитесь с матрицами" ("Matrices can be your Friends"), то вы понимаете, что вы можете нормализовать три ряда матрицы и выровнять оси так, что они станут взаимно перпендикулярными - и ваша матрица снова станет "чисто вращательной". Проделайте эту операцию в каждом кадре или хотя бы раз за несколько кадров и никто не заметит никаких искажений.

Это работает вполне приемлемо, но я заметил, что многие люди находят это плохо работающим в системах, в которых текущее вращение известно только в виде матрицы. Если, к примеру, вы хотите создать компас, то вам необходимо знать, как ориентирована сцена. Но получить эту информацию непосредственно из матрицы нельзя. Поэтому многие люди конвертируют матрицу обратно, в углы Эйлера.

  ...конвертируем первоначальную позицию в матрицу 'position' ;

  while ( 1 )
  {
    ...считываем изменения углов поворота рукоятки джойстика или другого устройства ввода ;
    ...конвертируем изменения углов  в матрицу 'velocity' ;
    ...перемножаем матрицы 'position' и 'velocity' ;

    ...нормализуем полученную матрицу ;
    ...конвертируем эту матрицу в углы Эйлера ;

    ...инвертируем матрицу ;
    ...помещаем матрицу на верх стека GL_MODELVIEW ;

    ...рисуем сцену ;
  }

Если вы собираетесь сделать что - нибудь вроде этого, то тогда вы можете делать и так:


  while ( 1 )
  {
    ...считываем изменения углов поворота рукоятки джойстика или другого устройства ввода ;
    ...конвертируем изменения углов  в матрицу 'velocity' ;
    ...конвертируем  текущие углы Эйлера в матрицу 'position' ;
    ...перемножаем матрицы 'position' и 'velocity' ;
    ...конвертируем эту матрицу в углы Эйлера ;

    ...инвертируем матрицу ;
    ...помещаем матрицу на верх стека GL_MODELVIEW ;

    ...рисуем сцену ;
  }

Вы экономите время, отказавшись от запутанной нормализации матрицы, так как в каждом кадре матрица восстанавливается из Эйлеровых углов. Однако, надо принять меры предосторожности, поскольку при конвертировании матрицы в Эйлеровы углы мы получаем несколько "неизвестных" значений при некоторых ориентациях. Если позиция точки наблюдения повернута на 90 градусов по углу рысканья, тогда изменения в углах крена и тангажа произведут правильный эффект. Следовательно, хотя и крен + тангаж == constant, разница между ними нечёткая и может полностью изменяться от кадра к кадру. Если вы используете только угол рысканья для создания, скажем, компаса, тогда ждите невероятных показаний при некоторых углах Крена и Тангажа. Такое случается и в реальной жизни - так что особо не переживайте об этом!

Кватернионы.

В дополнение к Эйлеровым углам и матричным преобразованиям, существует ещё один, очень удобный способ представления вращений. Кватернион (как это и видно по названию) представляет собой набор из четырёх чисел, которые могут быть представлены как вектор (X,Y,Z) единичной длины и угол вращения вокруг этого вектора. Комбинации перемещений вектора и изменения угла кодируют любые возможные вращения.

Это та же самая идея, что и в четырёх параметрах функции glRotate (хотя и четыре числа кватерниона, для удобства в расчётах, имеют другое значение).

Я не собираюсь здесь детально описывать кватернионы, но они действительно удобны в использовании, поскольку (подобно матрицам) кодируют углы независимо от порядка ввода, и , вдобавок, (подобно Эйлеровым углам) могут ЕДИНСТВЕННЫМ ОБРАЗОМ закодировать вращения, и, следовательно гораздо менее восприимчивы к ошибкам округления (хотя вам необходимо проверять векторную часть кватерниона - его длина всегда должна оставаться равной 1.0 - это гораздо проще сделать, чем нормализовать матрицу).


Другие статьи для программистов...




Copyright © 2007 г. РОССПРОГРАММПРОДУКТ ®.


Rambler's Top100