Как заставить объект вести себя как дочерний, не делая его таковым?
Я пробую перемещать объект B относительно объекта А так, как если бы объект В был дочерним по отношению к А. Получается не совсем корректно.
В чём глобальная задача? Я поднимаю VR-контроллером объект со стола (например, гайку) и перемещаю его по сцене. Звучит просто. В сцене есть GameObject _cursor, позицию и поворот которого я получаю от устройства ввода (VR-контроллер). С помощью Physics.OverlapSphere нахожу коллайдер объекта, который хочу переместить.
И решение «в лоб» состоит в том, чтобы сделать перемещаемый объект дочерним по отношению к _cursor. В ассетах Oculus для Unity как раз реализован подобный способ. Проблема в том, что перемещаемый объект должен взаимодействовать с другими объектами сцены - не пролетать сквозь стол и другие объекты. Т. е. в нем должен быть включен коллайдер. Проблема в том, что такой объект сносит абсолютно все другие незамороженные объекты сцены, имеющие коллайдер и Rigidbody. От этого хотелось избавиться.
Решение следующее - перемещать объект с помощью Rigidbody.MovePosition, предварительно задав ему достаточно маленькую массу. Но как определить, куда его нужно переместить? Ведь я не хочу transform перемещаемого объекта «притягивать» к _cursor. Изначально на ум пришло использование «прокси» - пустого GameObject, дочернего к _cursor. При «хватании» этот прокси-объект принимал значение transform.position от схваченного объекта. И в дальнейшем перемещаемый объект методом Rigidbody.MovePosition перемещался в позицию прокси объекта. И в принципе, это работает. Но хочется избавиться от дополнительного объекта-пустышки.
Решение следующее. При «хватании» получить вектор от transform схваченного объекта до transform объекта _cursor:
Vector3 direction = _target.transform.position - _cursor.transform.position;
а также
Quaternion _oldRotation = _cursor.transform.rotation;
При обновлении кадра делаю следующее:
private void Update()
{
/* Здесь получаем вращение _cursor относительно предыдущего кадра */
Quaternion _difRotation = Quaternion.Inverse(_oldRotation) * _cursor.transform.rotation;
_oldRotation = _cursor.transform.rotation;
/* Здесь поворачиваем вектор кватернионом */
direction = _difRotation * direction;
/*
* Здесь перемещаем объект с учетом позиции _cursor в пространстве сцены
* и повернутого вектора direction.
* Для упрощения не используем Rigidbody.MovePosition
*/
_target.position = _cursor.transform.position + direction;
}
Результат выглядит правдоподобно, но есть «нюансы». Для наглядности я собрал упрощенную сцену, использующую такой подход. Здесь ref выступает в роли _cursor, а base - перемещаемый объект. Объект sub - дочерний к base и нужен для большей наглядности проблемы. А проблема в том, что пока ref вращается вокруг осей (x, y, z), всё выглядит идеально.
Но стоит сделать произвольный поворот, как всё ломается. Что странно, ведь я использую кватернион, который не должен зависеть от вращения через углы Эйлера.
Что я делаю не так? И как ещё можно решить вопрос, вынесенный в заголовок?
Ответы (1 шт):
Ошибка кроется здесь:
Quaternion _difRotation = Quaternion.Inverse(_oldRotation) * _cursor.transform.rotation;
Рассмотрим пример:
new = q * old
где new - кватернион в новом кадре, old - в старом, а q - вращение между кадрами.
new = q * old,
домножим обе части на Inverse(old) справа, то есть на обратную матрицу old
new * Inverse(old) = q * old * Inverse(old)
далее учитывая что old * Inverse(old) это единичная матрица, и на то, что мы вправе поставить скобки так:
new * Inverse(old) = q * (old * Inverse(old))
опираясь на ассоциативность кватернионов
new * Inverse(old) = q;
q = new * Inverse(old);
Таким образом получаем:
Quaternion _difRotation = _cursor.transform.rotation * Quaternion.Inverse(_oldRotation);
Учитывая некоммутативность произведения кватернионов, логичночто вы получили другой результат.
И в приведенном вами куске кода не увидел, где меняется rotation у _target (видимо где-то в другом месте).
Вот на всякий случай код, который работает:
using UnityEngine;
public class Rotator : MonoBehaviour
{
[SerializeField]
private Transform _target;
[SerializeField]
private Transform _cursor;
private Quaternion _oldRotation;
private Vector3 direction;
private void Update()
{
direction = _target.transform.position - _cursor.transform.position;
/* Здесь получаем вращение _cursor относительно предыдущего кадра */
Quaternion _difRotation = _cursor.transform.rotation * Quaternion.Inverse(_oldRotation); // 1
_oldRotation = _cursor.transform.rotation;
/* Здесь поворачиваем вектор кватернионом */
direction = _difRotation * direction;
/*
* Здесь перемещаем объект с учетом позиции _cursor в пространстве сцены
* и повернутого вектора direction.
* Для упрощения не используем Rigidbody.MovePosition
*/
_target.position = _cursor.transform.position + direction;
_target.rotation = _difRotation * _target.rotation; // 2
}
}



