Подтвердить что ты не робот

Правильное преобразование node относительно заданного пространства?

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

Как правильно перевести/повернуть node относительно родительского node в графе сцены?

Проблема

Рассмотрим следующую диаграмму молекулы воды (без соединительных линий) для родительской/дочерней структуры узлов сцены, причем O xygen атом является родительским node и 2 H атомы идрогена являются дочерними узлами.

молекула воды, представляющая родительские/дочерние элементы  node отношения

Проблема с переводом

Если вы захватите родительский O xygen атом и переведите структуру, вы ожидаете, что дети H ydrogen последуют за ними и останутся в одинаковой относительной позиции от своего родителя. Если вместо этого вы захватите дочерний элемент H и переведите его, тогда будет затронут только ребенок. Обычно это работает. Когда транслируются атомы O, атомы H автоматически перемещаются вместе с ним, как и ожидалось из иерархического графика.

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

Проблема с вращением

Если вы захватите родительский O node и повернете его, вы ожидаете, что дочерние узлы H также будут вращаться, но на орбите, поскольку вращение выполняемый родителем. Это работает по назначению.

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

Я действительно надеюсь, что это описание достаточно справедливо, но дайте мне знать, если это не так, и я уточню по мере необходимости.

Математика

Я использую матрицы размером 4x4 столбца (т.е. Matrix4) и векторы столбцов (т.е. Vector3, Vector4).

Ниже приведена некорректная логика, которая ближе всего подходит к правильному поведению. Обратите внимание, что я решил использовать синтаксис, подобный Java, с перегрузкой оператора, чтобы упростить чтение математики. Я пробовал разные вещи, когда думал, что понял, но я этого не сделал.

Текущая логика перевода

translate(Vector3 tv /* translation vector */, TransformSpace relativeTo):
    switch (relativeTo):
        case LOCAL:
            localTranslation = localTranslation * TranslationMatrix4(tv);
            break;
        case PARENT:
            if parentNode != null:
                localTranslation = parentNode.worldTranslation * localTranslation * TranslationMatrix4(tv);
            else:
                localTranslation = localTranslation * TranslationMatrix4(tv);
            break;
        case WORLD:
            localTranslation = localTranslation * TranslationMatrix4(tv);
            break;

Логика текущего вращения

rotate(Angle angle, Vector3 axis, TransformSpace relativeTo):
    switch (relativeTo):
        case LOCAL:
            localRotation = localRotation * RotationMatrix4(angle, axis);
            break;
        case PARENT:
            if parentNode != null:
                localRotation = parentNode.worldRotation * localRotation * RotationMatrix4(angle, axis);
            else:
                localRotation = localRotation * RotationMatrix4(angle, axis);
            break;
        case WORLD:
            localRotation = localRotation * RotationMatrix4(angle, axis);
            break;

Вычисление космических преобразований мира

Для полноты преобразования мира для this node вычисляются следующим образом:

if parentNode != null:
    worldTranslation = parent.worldTranslation * localTranslation;
    worldRotation    = parent.worldRotation    * localRotation;
    worldScale       = parent.worldScale       * localScale;
else:
    worldTranslation = localTranslation;
    worldRotation    = localRotation;
    worldScale       = localScale;

Кроме того, полное/скопированное преобразование node для this:

Matrix4 fullTransform():
    Matrix4 localXform = worldTranslation * worldRotation * worldScale;

    if parentNode != null:
        return parent.fullTransform * localXform;

    return localXform;

Когда требуется преобразование node для отправки в форму шейдера OpenGL, используется матрица fullTransform.

4b9b3361

Ответ 1

Я нашел ответ Николь Боласа, чтобы быть немного полезной, хотя все еще было несколько деталей, о которых я не был так понятен. Но этот ответ помог мне увидеть нетривиальный характер проблемы, над которой я работал, поэтому я решил упростить ситуацию.

Упрощенное решение - всегда в родительском пространстве

Я удалил Node.TransformSpace, чтобы упростить эту проблему. Все преобразования теперь применяются относительно родительского пространства Node, и все работает должным образом. Изменения структуры данных, которые я намеревался выполнить после того, как работа над работой (например, замена локальных матриц трансляции/масштабирования для простых векторов), теперь также имеются.

Ниже приводится сводка обновленной математики.

Обновлен перевод

A Node позиция теперь представлена ​​объектом Vector3, при построении Matrix4 по требованию (см. ниже).

void translate(Vector3 tv /*, TransformSpace relativeTo */):
    localPosition += tv;

Обновлено вращение

Теперь вращение содержится в Matrix3, т.е. матрица 3x3.

void rotate(Angle angle, Vector3 axis /*, TransformSpace relativeTo */):
    localRotation *= RotationMatrix3(angle, axis);

Я по-прежнему планирую посмотреть на кватернионы позже, после того, как я смогу проверить правильность преобразований матрицы в кватернионе < =.

Обновлено масштабирование

Как и позиция Node, масштабирование теперь также является объектом Vector3:

void scale(Vector3 sv):
    localScale *= sv;

Обновленные вычисления преобразования локальных/мировых

Следующие обновления a Node world преобразуется относительно своего родителя Node, если таковой имеется. Проблема здесь была устранена путем удаления ненужной конкатенации для полного преобразования родителя (см. Оригинальное сообщение).

void updateTransforms():
    if parentNode != null:
         worldRotation = parent.worldRotation * localRotation;
         worldScale    = parent.worldScale    * localScale;
         worldPosition = parent.worldPosition + parent.worldRotation * (parent.worldScale * localPosition);
    else:
        derivedPosition = relativePosition;
        derivedRotation = relativeRotation;
        derivedScale    = relativeScale;

    Matrix4 t, r, s;

    // cache local/world transforms
    t = TranslationMatrix4(localPosition);
    r = RotationMatrix4(localRotation);
    s = ScalingMatrix4(localScale);
    localTransform = t * r * s;

    t = TranslationMatrix4(worldPosition);
    r = RotationMatrix4(worldRotation);
    s = ScalingMatrix4(worldScale);
    worldTransform = t * r * s;

Ответ 2

worldTranslation = parentNode.worldTranslation * localTranslation;
worldRotation    = parentNode.worldRotation    * localRotation;
worldScale       = parentNode.worldScale       * localScale;

Это не так, как работает накопление последовательных преобразований. И очевидно, почему нет, если вы думаете об этом.

Скажем, у вас есть два узла: родительский и дочерний. Родитель имеет 90-градусное вращение против часовой стрелки вокруг оси Z. У ребенка есть смещение +5 по оси X. Ну, вращение против часовой стрелки должно заставить его иметь +5 по оси Y, да (предполагая правую систему координат)?

Но это не так. На ваш localTranslation никогда не влияет какая-либо форма вращения.

Это верно для всех ваших преобразований. Переводы затрагиваются только переводами, а не масштабами или вращениями. Переводы не влияют на переводы. Etc.

Это то, что говорит ваш код, и это не то, как вы должны это делать.

Сохранение разложения компонентов ваших матриц - хорошая идея. То есть, наличие отдельных компонентов перевода, вращения и масштабирования (TRS) является хорошей идеей. Это упрощает применение последовательных локальных преобразований в правильном порядке.

Теперь, сохраняя компоненты как матрицы, неправильно, потому что это действительно не имеет смысла и тратит время и пространство на нет реальной причины. Перевод всего лишь vec3, и ничего не получится, сохранив с ним 13 других компонентов. Когда вы скопируете переводы локально, вы просто добавляете их.

Однако в тот момент, когда вам нужно аккумулировать окончательную матрицу для node, вам необходимо преобразовать каждую декомпозицию TRS в свою собственную локальную матрицу, а затем преобразовать ее в родительское общее преобразование, а не в родительские отдельные компоненты TRS. То есть вам нужно составить отдельные преобразования локально, а затем умножить их на матрицу родительского преобразования. В псевдокоде:

function AccumRotation(parentTM)
  local localMatrix = TranslationMat(localTranslation) * RotationMat(localRotation) * ScaleMat(localScale)
  local fullMatrix = parentTM * localMatrix

  for each child
    child.AccumRotation(fullMatrix)
  end
end

Каждый родитель передает свой собственный накопленный поворот ребенку. Корню node задана единичная матрица.

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

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

function TranslateWorld(transVec)
  local parentMat = this->parent ? this->parent.ComputeTransform() : IdentityMatrix
  local localMat = this->ComputeLocalTransform()
  local offsetMat = TranslationMat(localTranslation)
  local myMat = parentMat.Inverse() * offsetMat * parentMat * localMat
end

Значение функции P -1 OP фактически является общей конструкцией. Это означает преобразование общего преобразования O в пространство P. Таким образом, он преобразует мировое смещение в пространство родительской матрицы. Затем мы применяем это к нашему локальному преобразованию.

myMat теперь содержит матрицу преобразования, которая при умножении на родительские преобразования будет применять transVec, как если бы она находилась в мировом пространстве. Это то, что вы хотели.

Проблема заключается в том, что myMat является матрицей, а не разложением TRS. Как вы вернетесь к разложению TRS? Ну... это требует действительно нетривиальной математической матрицы. Для этого требуется сделать что-то, называемое Разложение сингулярных значений. И даже после реализации уродливой математики SVD может потерпеть неудачу. Возможно иметь неразложимую матрицу.

В графической системе сцены, которую я написал, я создал специальный класс, который был фактически объединением между декомпозицией TRS и матрицей, которую он представляет. Вы можете запросить, была ли она разложена, и если бы вы могли изменить компоненты TRS. Но как только вы попытались присвоить ему значение матрицы 4x4, он стал составной матрицей, и вы больше не сможете применять локальные разложенные преобразования. Я даже не пытался реализовать SVD.

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

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

Ответ 3

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

Предположим, что у вас есть матрица X и произведение матрицы ABC. И предположим, что вы хотите размножить найти Y так, что

X*A*B*C = A*B*Y*C

или наоборот.

Предполагая, что матрицы не являются сингулярными, сначала исключим общие термины:

X*A*B = A*B*Y

Затем выделите. Следя за левым и правым, умножьте на инверсии:

A^-1*X*A*B = A^-1 *A *B *Y
A^-1*X*A*B = B *Y
B^-1*A^-1*X*A*B = Y

или в случае, когда у вас есть Y, но X:

X*A*B *B^-1 *A^-1 = A*B*Y*B^-1 *A^-1
X = A*B*Y*B^-1 *A^-1

Вышеприведенное является лишь частным случаем общего правила:

X*A = A*Y

Средства

X=A*Y*A^-1
A^-1*X*A=Y

С учетом того, что (A*B)^-1 = B^-1 * A^-1.

Эта процедура позволяет вам проверить цепочку преобразований и спросить: "Я хочу применить преобразование в определенном месте, но сохраните его, применив его в другом месте". Это является основной причиной вашей проблемы.

Цепочка матриц, с которыми вы работаете, должна включать в себя все преобразования - переводы, вращения, масштабы - не только преобразования одного и того же типа, так как решение для X * B = B * Y не дает решения для X * A * B = A * B * Y.