Нет, это не ошибка движка или артефакт определенного представления вращения (они тоже могут быть, но этот эффект применяется к любой системе, которая представляет вращения, включая кватернионы).
Вы обнаружили реальный факт о том, как вращение работает в трехмерном пространстве, и он отличается от нашей интуиции в отношении других преобразований, таких как перевод:
Когда мы составляем повороты по нескольким осям, мы получаем не просто общее / чистое значение, которое мы применили к каждой оси (как мы могли бы ожидать для перевода). Порядок, в котором мы применяем повороты, изменяет результат, так как каждое вращение перемещает оси, к которым применяются следующие повороты (если вращаются вокруг локальных осей объекта), или взаимосвязь между объектом и осью (если вращается вокруг мира оси).
Изменение отношения осей с течением времени может сбить с толку нашу интуицию о том, что каждая ось «должна» делать. В частности, определенные комбинации поворотов рыскания и тангажа дают тот же результат, что и вращение крена!
Вы можете убедиться, что каждый шаг вращается правильно относительно запрошенной оси - в наших обозначениях нет ни сбоев двигателя, ни артефактов, которые мешали бы или вводили в заблуждение наши данные - сферическая (или гиперсферическая / кватернионная) природа вращения просто означает, что наши преобразования "обертывают вокруг "друг на друга. Они могут быть ортогональны локально, для небольших вращений, но когда они накапливаются, мы обнаруживаем, что они не являются ортогональными в глобальном масштабе.
Это наиболее драматично и ясно для поворотов на 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 или вручную создавая ортонормированную матрицу из этих векторов) Управляя вектором вверх, мы контролируем поворот или поворот.
Для примера полета / плавания вы захотите применять эти поправки постепенно с течением времени. Если оно слишком резкое, вид может отвлекать внимание. Вместо этого вы можете использовать текущий вектор игрока вверх и показывать его по вертикали, пока кадр не выровняется. Применение этого во время поворота иногда может быть менее тошнотворным, чем поворот камеры, когда элементы управления игрока находятся в режиме ожидания.