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

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

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

Проблема

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

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

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

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

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

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

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

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

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

Математика

Я использую матрицы 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 вычисляются следующим образом:

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

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

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

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

    return localXform;

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


person code_dredd    schedule 21.06.2016    source источник
comment
Не ответ, но рассматривали ли вы возможность использования кватернионов, чтобы избежать постепенной потери точности?   -  person o11c    schedule 25.06.2016
comment
Давным-давно я сделал похожую программу (интерактивное манипулирование химическими диаграммами). Я использовал простую модель шара и пружин (с динамическими виртуальными пружинами для поддержания отображаемых углов) при перемещении атомов, а также модель твердого тела (каждый атом имеет положение в 2D или 3D объеме, а объемная коробка управляется с помощью стандартных жестких формулы тела, подобные которым вы можете найти повсюду) при перемещении целых молекул. Короче говоря: работая со своими атомами по отдельности, вы делаете это сложнее, чем нужно. Никогда не думайте, что вращение и перемещение — разные задачи.   -  person Dave    schedule 25.06.2016
comment
@ o11c: я хотел использовать кватернионы, чтобы обеспечить плавную интерполяцию, особенно когда к узлу подключена камера, и вы хотите переместить камеру с помощью узла. Но в настоящее время я отслеживаю проблему, которая кажется в преобразовании кватерион-›матрица, которое, кажется, создает странную плоскость отсечения в области усеченного обзора камеры. Я предполагаю, что преобразование где-то неправильно... хотя я пробовал довольно много вещей. Я думаю, что мне придется опубликовать вопрос об этом в ближайшее время.   -  person code_dredd    schedule 26.06.2016
comment
@Dave: Не могли бы вы быть более конкретным? Молекула здесь — это просто визуальный способ объяснить, как организованы мои родительские/дочерние узлы в графе сцены, и я не уверен, что следую части Никогда не предполагайте, что вращение и перемещение — разные задачи. Можете быть более конкретными? Вы заметили проблему в математике или догадались?   -  person code_dredd    schedule 26.06.2016
comment
Извините, я не просмотрел ваш код. Рассматривали ли вы возможность использования библиотеки для решения сложных задач за вас? Большинство 3D-движков имеют подпрограммы для этих задач преобразования, которые уже были тщательно разработаны и протестированы (а также изначально используют кватернионы и обрабатывают всю эту логику для вас). Если вы действительно хотите сделать это самостоятельно, я бы посоветовал вам немного сесть с ручкой и бумагой и начать с нуля (при работе над сложной проблемой легко застрять в менталитете особого случая / небольшой настройки, когда вы d лучше подойти к этому с другого угла).   -  person Dave    schedule 26.06.2016
comment
@Dave: я использую библиотеку для обработки матриц, векторов и т. д. Моя математическая проблема заключается в том, что я не понимаю, как работают матрицы и векторы. Насколько я понимаю, здесь необходимо объединить матрицы, чтобы правильно преобразовать объект (то есть узел) относительно источника его родительского узла. Я знаю, как выполнять матричное умножение и т. д. (в библиотеке также проходят автоматические тесты.) Есть и другие требования (например, язык, лицензия и т. д.). Я читал на математических сайтах, но они сосредоточены на вещах, которые я м уже знакомы и делают правильно. Я чувствую, что мы можем немного отвлечься.   -  person code_dredd    schedule 26.06.2016


Ответы (3)


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

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

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

Но это не так. Ваш localTranslation никогда не подвергается какой-либо ротации.

Это относится ко всем вашим трансформациям. На переводы влияют только переводы, а не масштабы или повороты. Вращения не зависят от переводов. И Т. Д.

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

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

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

Однако в момент, когда вам нужно накопить окончательную матрицу для узла, вам нужно преобразовать каждую декомпозицию 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

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

Декомпозиция 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-1OP на самом деле является общей конструкцией. Это значит преобразовать общее преобразование O в пространство P. Таким образом, он преобразует мировое смещение в пространство родительской матрицы. Затем мы применяем это к нашему локальному преобразованию.

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

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

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

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

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

person Nicol Bolas    schedule 26.06.2016
comment
Я отредактировал сообщение, чтобы прояснить то, что, возможно, было неясно. Кажется, я уже делал что-то похожее на вашу 1-ю рекомендацию в соответствии с AccumRotation в моей реализации для fullTransform, где конкатенированная локальная форма xform объединяется с родительской матрицей fullTransform, если она присутствует. Кажется, это решает проблему перевода у детей. (Я по ошибке объединил мировые, а не локальные матрицы TRS.) Я все еще просматриваю остальную часть вашего поста и проблему вращения. - person code_dredd; 27.06.2016
comment
Кажется, я не полностью понял вторую часть вашего объяснения, так как не могу заставить его работать правильно. Я удалил Node.TransformSpace на данный момент, чтобы не застрять дольше, чем я уже был. Я обновил преобразования по-другому, и, похоже, он работает правильно. - person code_dredd; 27.06.2016

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

Простое решение — всегда в родительском пространстве

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

Краткое изложение обновленной математики следует.

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

Позиция 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;

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

Следующее обновляет преобразования мира Node относительно его родителя 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;
person code_dredd    schedule 02.07.2016

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

Предположим, у вас есть матрица 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.

person Yakk - Adam Nevraumont    schedule 26.06.2016
comment
Есть вещи, в которых я до сих пор не разобрался; пожалуйста, отредактируйте для уточнения/удобочитаемости. Я заявил, что использую матрицы/векторы с основными столбцами, но ваша математика, похоже, представляет собой смесь основных интерпретаций столбцов/строк, и в ней отсутствуют операторы, что несовместимо и не соответствует различию. Кроме того, когда вы говорите, что в моем уравнении отсутствует обратное: отсутствует где именно? Кроме того, я знаю, что матричные операции не являются коммутативными, пожалуйста, объясните, почему/как это заставляет это делать; Я не уследил за этим. Узлы также возвращают свой полный перевод путем конкатенации Tw * Rw * Sw (w=world). Есть несколько причин хранить матрицы T*R*S отдельно. - person code_dredd; 26.06.2016
comment
Я хотел сказать, что узлы также возвращают свои полные преобразования (не перевод) путем объединения матриц Tw * Rw * Sw. Поскольку они представляют собой матрицы столбцов 4x4 и читаются справа налево, порядок их применения эквивалентен Tw * (Rw * Sw). - person code_dredd; 26.06.2016
comment
@ray представьте, что кто-то опубликовал код, в котором было полно ручных циклов с помощью переходов, а сложение и сравнение выполнялись побитовыми операциями, написанными вручную в каждом месте. И это не сработало. Вы можете заметить, что по крайней мере одна из битовых операций имеет ошибку, потому что они не имеют логики переноса, но не могут следовать тому, что должно быть. Вы указали, что использование управления потоком и стандартных операций в коде будет работать лучше. Они отвечают, что goto и битовые операции имеют определенные преимущества, и просят вас сказать, откуда они должны получить перенос, и они используют сложение с дополнением 3s. - person Yakk - Adam Nevraumont; 26.06.2016
comment
Даже если я помещу все в единую матрицу, и она вдруг заработает, я на самом деле не исправлю математическое недоразумение, которое у меня сейчас есть. Я думаю, что ваш комментарий пропускает это. Есть также вещи, которые кажутся более сложными, например. больше не может устанавливать абсолютную мировую позицию, просто создавая новую матрицу перевода из вектора b/c. Я бы потерял преобразования вращения/масштабирования. Мне также трудно следовать вашему объяснению из-за сочетания столбцов и строк, которое у вас есть в вашей математике. К сожалению, ответ не слишком полезен для меня в его нынешнем состоянии. - person code_dredd; 26.06.2016
comment
Стоит отметить, что я только что закончил пробовать подход с одной матрицей, но вместо этого наблюдались другие проблемы, особенно в дочерних узлах. Вы могли бы сказать, что я просто неправильно вас понял; возможно, ты прав. Если бы вы могли уточнить и уточнить свой пост, это было бы более полезным. - person code_dredd; 26.06.2016
comment
Извините, я не знаю жаргона по строкам/столбцам. Похоже, что это способ описания openGL (и, возможно, другой), если вы рассматриваете вектор как столбец или матрицу строк? Это кажется в основном неуместным для математики. Я сделаю свой ответ более абстрактным, удалив все биты, связанные с порядком умножения и т. д. - person Yakk - Adam Nevraumont; 26.06.2016
comment
Я думаю, я увижу ваше редактирование позже сегодня, так как я называю это ночью, но обратите внимание, что способ, которым записываются/читаются умножения между матрицами и векторами, зависит от того, используете ли вы основные столбцы или строки. Например, при использовании организации по столбцам (что и использую я), умножение матрицы на вектор должно быть записано как v' = M*v, тогда как запись v' = v*M подразумевает организацию по строкам. Это стандарт в математических текстах. Надеюсь, это поможет устранить двусмысленность, и поскольку эти операции не являются коммутативными, это действительно имеет значение для математики. - person code_dredd; 26.06.2016
comment
Column-major — это стандартный способ для компьютерных графических приложений, но Direct X работает по-другому и в далеком прошлом набрал некоторую скорость, потому что аппаратное обеспечение иногда лучше поддерживает это. Неудивительно, что мажор строк также широко используется. Вы будете удивлены, узнав, сколько странных вещей происходит внутри графического драйвера. Ответ Якка довольно хорош. Чтобы найти v в v'=M*v, вы делаете то же самое: M^(-1) * v' = M^(-1) * M * v. При необходимости вы, вероятно, захотите разложить их на масштаб, вращение и подматрицы перевода, но их инвертирование все же требуется. - person StarShine; 29.06.2016