|
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).)
Фактически, вся проблема состоит в том, что когда вы комбинируете эти три вращения, вы ДОЛЖНЫ делать это в особом порядке -
вы можете выбрать любой порядок - но математика всегда вынуждает вас сделать конкретный выбор.
То есть вы можете совершить повороты в таком порядке: сначала крен, затем тангаж и в конце рысканье.
Теперь посмотрим, что может случиться с такой последовательностью операций - и, для простоты, рассмотрим только крен и тангаж.
(Если вы - правша, то вам гораздо легче будет следить за ходом обсуждения, если вы сложите самолётик из бумаги. Сложите один сейчас же!
Левши обычно имеют лучшее пространственное воображение и могут во всём разобраться мысленно :-)
- Самолёт (при отсутствии вращений) горизонтально выровнен и направлен носом прямо на Север.
- Зададим углы Рысканья/Тангажа/Крена - (0,90,90) и посмотрим, что случится:
- Самолёт теперь имеет крен в 90 градусов (так , что правый конец крыла указывает на небо, а левый на землю).
- ...и тангаж в 90 градусов (имеется ввиду, что нос поднят ВВЕРХ) ...так, что нос самолёта указывает на Запад, а хвост направлен на Восток,
правое крыло по-прежнему направлено вверх, и левое - вниз.
- Теперь пилот пытается выровнять самолёт по горизонтали, поворачивая рукоятку джойстика направо,
- что в соответствии с вышеприведённым кодом должно постепенно уменьшить крен самолёта до нуля.
Вы можете ожидать в результате, что нос самолёта будет по-прежнему направлен к Западу, - но на самом-то деле теперь нос самолёта и крен...
- Так, в конце этого маневра, HPR - углы изменятся от (0,90,90) до (0,90,0) - каково теперь на самом деле положение самолёта в пространстве?
- Давайте вернёмся назад, к первому пункту, (самолёт горизонтально выровнен и направлен носом к Северу) и определим положение самолёта, если HPR - углы заданы, как (0,90,0).
- Отлично, нулевой крен задаёт правильное положение крыльев - левое крыло направлено
к западу и правое к востоку... и 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 -
это гораздо проще сделать, чем нормализовать матрицу).
Другие статьи для программистов...
|
|