Я вращаю объект по двум осям, так почему же он продолжает вращаться вокруг третьей оси?

38

Я вижу, что довольно часто возникают вопросы, связанные с этой основной проблемой, но все они связаны с особенностями данной функции или инструмента. Вот попытка создать канонический ответ, к которому мы можем отсылать пользователей, когда это произойдет, - с множеством анимированных примеров! :)


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

void Update() {
    float speed = lookSpeed * Time.deltaTime;

    // Yaw around the y axis using the player's horizontal input.        
    transform.Rotate(0f, Input.GetAxis("Horizontal") * speed, 0f);

    // Pitch around the x axis using the player's vertical input.
    transform.Rotate(-Input.GetAxis("Vertical") * speed,  0f, 0f);
}

или, может быть

// Construct a quaternion or a matrix representing incremental camera rotation.
Quaternion rotation = Quaternion.Euler(
                        -Input.GetAxis("Vertical") * speed,
                         Input.GetAxis("Horizontal") * speed,
                         0);

// Fold this change into the camera's current rotation.
transform.rotation *= rotation;

И это в основном работает, но со временем вид начинает изгибаться. Кажется, что камера вращается вокруг своей оси вращения (z), хотя мы только сказали ей вращаться по осям x и y!

Анимированный пример наклона камеры от первого лица в сторону

Это также может произойти, если мы пытаемся управлять объектом перед камерой - скажем, это глобус, который мы хотим повернуть, чтобы осмотреться:

Анимированный пример поворота шара в сторону

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

Во многих движках вы также увидите это в инспекторе - вращайте объект в мире, и внезапно числа меняются на оси, которую мы даже не трогали!

Анимированный пример, показывающий манипулирование объектом рядом со считыванием его углов поворота.  Угол z изменяется, даже если пользователь не манипулировал этой осью.

Итак, это ошибка двигателя? Как нам сказать программе, что мы не хотим, чтобы она добавляла дополнительное вращение?

Это как-то связано с углами Эйлера? Стоит ли вместо этого использовать кватернионы, матрицы вращения или базисные векторы?

Д.М.Григорий
источник
1
Я также нашел очень полезным ответ на следующий вопрос. gamedev.stackexchange.com/questions/123535/…
Трэвис Петри

Ответы:

51

Нет, это не ошибка движка или артефакт определенного представления вращения (они тоже могут быть, но этот эффект применяется к любой системе, которая представляет вращения, включая кватернионы).

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

Анимированный пример, показывающий, что применение поворотов в другом порядке дает разные результаты

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

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

Анимированный пример, показывающий последовательность локального шага-рыскания-тона, дает тот же результат, что и один локальный бросок

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

Это наиболее драматично и ясно для поворотов на 90 градусов, как те, что указаны выше, но блуждающие оси также проникают в течение многих малых поворотов, как показано в вопросе.

Итак, что нам с этим делать?

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

Вот та же последовательность шага-рыскания-шага, которая стала креном в приведенном выше примере, но теперь мы применяем наш рыскание вокруг глобальной оси Y вместо объекта

Анимированный пример кружки, вращающейся с локальным шагом и глобальным рысканием, без проблем с креномАнимированный пример камеры от первого лица с использованием глобального рыскания

Таким образом, мы можем исправить камеру от первого лица с помощью мантры «Pitch Localally, Yaw Globally»:

void Update() {
    float speed = lookSpeed * Time.deltaTime;

    transform.Rotate(0f, Input.GetAxis("Horizontal") * speed, 0f, Space.World);
    transform.Rotate(-Input.GetAxis("Vertical") * speed,  0f, 0f, Space.Self);
}

Если вы составляете свои вращения, используя умножение, вы бы перевернули левый / правый порядок одного из умножений, чтобы получить тот же эффект:

// Yaw happens "over" the current rotation, in global coordinates.
Quaternion yaw = Quaternion.Euler(0f, Input.GetAxis("Horizontal") * speed, 0f);
transform.rotation =  yaw * transform.rotation; // yaw on the left.

// Pitch happens "under" the current rotation, in local coordinates.
Quaternion pitch = Quaternion.Euler(-Input.GetAxis("Vertical") * speed, 0f, 0f);
transform.rotation = transform.rotation * pitch; // pitch on the right.

(Конкретный порядок будет зависеть от соглашений умножения в вашей среде, но левый = более глобальный / правый = более локальный - это обычный выбор)

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

// Construct a new orientation quaternion or matrix from Euler/Tait-Bryan angles.
var newRotation = Quaternion.Euler(totalPitch, totalYaw, 0f);
// Apply it to our object.
transform.rotation = newRotation;

или эквивалентно ...

// Form a view vector using total pitch & yaw as spherical coordinates.
Vector3 forward = new Vector3(
                    Mathf.cos(totalPitch) * Mathf.sin(totalYaw),
                    Mathf.sin(totalPitch),
                    Mathf.cos(totalPitch) * Mathf.cos(totalYaw));

// Construct an orientation or view matrix pointing in that direction.
var newRotation = Quaternion.LookRotation(forward, new Vector3(0, 1, 0));
// Apply it to our object.
transform.rotation = newRotation;

При использовании этого глобального / локального разделения у вращений нет шансов объединиться и повлиять друг на друга, потому что они применяются к независимым наборам осей.

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

Анимированный пример глобуса, показывающего вращение с лучшим поведением

Ограничения

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

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

Например, мы можем использовать локальные повороты для обновления нашего «прямого» вектора, а затем использовать этот прямой вектор вместе со ссылочным «восходящим» вектором для построения нашей окончательной ориентации. (Используя, например, метод Unity Quaternion.LookRotation или вручную создавая ортонормированную матрицу из этих векторов) Управляя вектором вверх, мы контролируем поворот или поворот.

Для примера полета / плавания вы захотите применять эти поправки постепенно с течением времени. Если оно слишком резкое, вид может отвлекать внимание. Вместо этого вы можете использовать текущий вектор игрока вверх и показывать его по вертикали, пока кадр не выровняется. Применение этого во время поворота иногда может быть менее тошнотворным, чем поворот камеры, когда элементы управления игрока находятся в режиме ожидания.

Д.М.Григорий
источник
1
Могу я спросить, как ты сделал для GIFS? Они выглядят хорошо.
Балинт
1
@ Bálint Я использую бесплатную программу под названием LICEcap - она ​​позволяет записывать часть экрана в формате GIF. Затем я обрезаю анимацию / добавляю дополнительный текст заголовка / сжимаю рисунок с помощью Photoshop.
DMGregory
Этот ответ только для Unity? Есть ли более общий ответ в Интернете?
posfan12
2
@ posfan12 Пример кода использует синтаксис Unity для конкретности, но ситуации и математика в равной степени применимы ко всем. Есть ли у вас какие-либо трудности с применением примеров в вашей среде?
DMGregory
2
Математика уже присутствует выше. Судя по вашему редактированию, вы просто растираете не те его части. Похоже, вы используете тип вектора, определенный здесь . Это включает в себя способ построения матрицы вращения из набора углов, как описано выше. Начните с матрицы тождеств и вызовите TCVector::calcRotationMatrix(totalPitch, totalYaw, identity)- это эквивалентно приведенному выше примеру «все сразу Эйлера».
DMGregory
1

ПОСЛЕ 16 часов функций воспитания и ротации лол.

Я просто хотел, чтобы код был пустым, в основном, вращаясь по кругу, а также переворачивая фронт при повороте.

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

Вот те немногие, которые на самом деле работали в моем приложении

В единстве:

  1. Создать пустое. Назовите это ForYaw.
  2. Дайте этому ребенку пустой, называется ForPitch.
  3. Наконец, сделайте куб и переместите его к z = 5 (вперед). Теперь мы можем видеть то, что мы пытаемся избежать, - это будет перекручивать куб (случайный «крен», когда мы поворачиваемся и тангаем) .

Теперь создайте сценарии в Visual Studio, выполните настройку и в конечном итоге получите следующее:

objectForYaw.Rotate(objectForYaw.up, 1f, Space.World);
    objectForPitch.Rotate(objectForPitch.right, 3f, Space.World);

    //this will work on the pitch object as well
    //objectForPitch.RotateAround
    //    (objectForPitch.position, objectForPitch.right, 3f);

Обратите внимание, что все это сломалось для меня, когда я попытался изменить порядок воспитания. Или если я изменю Space.World для дочернего основного тона (родительский Yaw не возражает против поворота через Space.Self). Смешение. Я мог бы определенно использовать некоторые пояснения о том, почему мне пришлось взять локальную ось, но применить ее через мировое пространство - почему Space.Self все разрушает?

Джех
источник
Мне не ясно, предназначено ли это для ответа на вопрос, или если вы просите помощи, чтобы понять, почему написанный вами код работает таким образом. Если это последнее, вы должны опубликовать новый вопрос, со ссылкой на эту страницу для контекста, если хотите. Как подсказка, me.Rotate(me.right, angle, Space.World)это то же самое me.Rotate(Vector3.right, angle, Space.Self, что и ваши вложенные преобразования эквивалентны примерам "Pitch Localally, Yaw Globally" в принятом ответе.
DMGregory
Немного из обоих. Частично, чтобы поделиться в ответе (например, вот как именно это работает для меня). А также ввести вопрос. Этот намек заставил меня задуматься. Большое спасибо!
Джех
Удивительно, но за это не проголосовали. После долгих часов чтения исчерпывающих ответов, которые, хотя образовательные и не работали, простой ответ - использование оси вращения объектов вместо общей оси Vector3.right - все, что нужно, чтобы зафиксировать вращение на объекте. Невероятно то, что я искал, было похоронено здесь с 0 голосами и на 2-й странице Google много, много похожих вопросов.
user4779