Правильное преобразование 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
.
Ответы
Ответ 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
.