Как заставить объект вести себя как дочерний, не делая его таковым?

Я пробую перемещать объект 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
    }
}
→ Ссылка