Применение весов к матрицам и вершинам (вращение кости)
Я вращаю кости скелета внутри сетки для низкополигональной трехмерной фигуры. На вершинный шейдер его применяют вот так.
GLSL:
vec4 vert1 = (bone_matrix[index1]*vertex_in)*weight;
vec4 vert2 = (bone_matrix[index2]*vertex_in)*(1-weight);
gl_Position = vert1+vert2;
bone_matrix[index1]
является матрицей одной кости и bone_matrix[index2]
это матрица другого. weight
назначает в vertex_in
Причастность к этим костям. Проблема в том, что чем ближе вес к 0,5, тем больше диаметр колена сжимается при вращении. Я проверил это с примерно 10000 вершинных цилиндров (с градиентом веса). Результат выглядел как изгиб садового шланга.
Я получил свой метод взвешивания из этих источников. На самом деле это единственный способ найти:
http://www.opengl.org/wiki/Skeletal_Animation
http://ogldev.atspace.co.uk/www/tutorial38/tutorial38.html
http://blenderecia.orgfree.com/blender/skinning_proposal.pdf
Слева - как начинается форма, в середине - как вращается вышеприведенное уравнение, а справа - моя цель. Средние точки взвешены 0.5
, Это только ухудшается, чем больше это согнуто, в 180 градусах это имеет нулевой диаметр.
- Я попытался собрать матрицу в шейдере, чтобы я мог применить веса к вращению вместо результирующих вершин. Он выглядит идеально, как на рисунке справа, но требует сборки матрицы для каждой отдельной вершины (дорого)
- Я изучил кватернионы, но glsl не поддерживает их изначально (поправьте меня, если я ошибаюсь), и они сбивают с толку. Это то, что мне нужно сделать?
- Я рассмотрел три кости на сустав и добавил коленную чашечку между каждой костью. Это не устранит проблему, но уменьшит ее.
- Я рассматриваю проекцию вершины на ее первоначальное расстояние от оси после их поворота. Это потерпит неудачу при 180 градусах, но будет (относительно) дешевым.
Итак, рассматривая варианты или другие варианты, которые я, возможно, не рассмотрел, как другие могут избежать этого эффекта сжатия?
РЕДАКТИРОВАТЬ: я получил SLERP для работы с кватернионами, но я решил не использовать его, поскольку GLSL изначально не поддерживает его. Я не мог заставить геометрический SLERP работать, как описано Томом. Я получил NLERP, работающий на первые 90 градусов, поэтому я добавил дополнительную "кость" между каждым суставом. Таким образом, чтобы согнуть предплечье на 40 градусов, я сгибаю локоть и предплечье на 20 градусов каждый. Это устраняет эффект защемления за счет удвоения количества костей, что не является идеальным решением.
3 ответа
Эта проблема
Причину того, что вы видите, иллюстрирует рисунок в ответе Levans. Однако, чтобы понять, что происходит, подумайте о том, что происходит, когда вы выполняете код:
Если первая точка vert1
имеет координаты (p, 0)
координаты vert2
будет (p cos(α), p sin(α))
где α
это угол между двумя костями (это всегда возможно при соответствующем преобразовании координат). Добавление их вместе с использованием соответствующих весов w
а также 1-w
мы получаем следующие координаты:
x = w p + (1-w) p cos(α)
y = (1-w) p sin(α)
Длина этого вектора:
length^2 = x^2 + y^2
= (w p + (1-w) p cos(α))^2 + (1-w)^2 p^2 sin(α)^2
= p^2 [w^2 + 2 w (1-w) cos(α) + (1-w)^2 cos(α)^2 + (1-w)^2 sin(α)^2]
= p^2 [w^2 + (1-w)^2 + 2 w (1-w) cos(α)]
Как пример, когда w = 1/2
это упрощает до:
length^2 = p^2 (1/2 + 1/2 cos(α)) = p^2 cos(α/2)^2
А также length = p |cos(α/2)|
тогда как длина исходных векторов p
(см. график). Длина нового вектора становится меньше, это эффект сжатия, который вы восприняли. Причина этого в том, что мы на самом деле интерполируем две вершины вдоль прямой линии. Если мы хотим сохранить одинаковую длину p
нам на самом деле нужно интерполировать по кругу вокруг центра вращения. Один из возможных подходов заключается в перенормировке результирующего вектора с сохранением ширины в стыке.
Это означает, что нам нужно разделить полученные координаты вершины на |cos(α/2)|
(или более общий результат для произвольных весов). В качестве побочного эффекта это, конечно, деление на ноль, когда угол составляет ровно 180° (по той же причине ширина в соединении равна вашей технике).
Я не специалист по скелетной анимации, но мне кажется, что оригинальное решение, как вы его описали, является приближением для работы с малыми углами кости (где эффект сжатия минимален).
Альтернативные подходы
Другой подход заключается в интерполяции ваших вращений вместо вершин. Посмотрите, например, страницу вики и эту статью.
SLERP
Техника Slerp похожа на технику, которую я описал выше, в том смысле, что она также сохраняет ширину в суставе, однако она интерполируется непосредственно по круговой траектории вокруг сустава. Общая формула:
gl_Position = [sin((1-w)α)*vert1 + sin(wα)*vert2]/sin(α)
Учитывая очки сверху vert1 = (p, 0)
а также vert2 = (p cos(α), p sin(α))
применение формулы SLERP дает result = (x, y)
с:
x = p [sin((1-w)α) + sin(wα) cos(α)]/sin(α)
y = p sin(wα) sin(α)/sin(α) = p sin(wα)
Расчет косинуса cos θ
угла между vert1
а также result
выходы:
cos(θ) = vert1*result/(|vert1| |result|) = vert1*result/p^2
= p^2 [sin(wα) + sin((1-w)α) cos(α)]/sin(α)/p^2
= [sin(α) cos((1-w)α) - cos(α) sin((1-w)α) + sin((1-w)α) cos(α)]/sin(α)
= cos((1-w)α)
Угол между vert2
а также result
является:
cos(φ) = vert2*result/p^2
= [sin(wα) cos(α) + sin((1-w)α) cos(α)^2 + sin((1-w)α) sin(α)^2]/sin(α)
= [sin(wα) cos(α) + sin((1-w)α) cos(α)]/sin(α)
= [sin(wα) cos(α) + sin(α) cos(wα) - cos(α) sin(wα)]/sin(α)
= cos(wα)
Это означает, что θ/φ = (1-w)/w
который выражает тот факт, что SLERP интерполирует с постоянной радиальной скоростью. При работе с трехмерными матрицами вращения мы можем выразить преобразование вращения vert1
в vert2
как M = inverse(A)*B = transpose(A)*B
так что мы можем выразить угол поворота α
как:
cos(α) = (tr(M) - 1)/2 = (tr(transpose(A)*B) - 1)/2
= (A[0][0]*B[0][0] + A[0][1]*B[1][0] + A[0][2]*B[2][0] +
A[1][0]*B[0][1] + A[1][1]*B[1][1] + A[1][2]*B[2][1] +
A[2][0]*B[0][2] + A[2][1]*B[1][2] + A[2][2]*B[2][2] - 1)/2
Quaternion LERP
При работе с кватернионами хорошее приближение к SLERP состоит в линейной интерполяции кватернионов непосредственно, после чего вы перенормируете результат. Это дает интерполяционную кривую, идентичную кривой в SLERP, однако интерполяция не происходит при постоянной радиальной скорости.
Если вы действительно хотите полностью избежать этих проблем, вы всегда можете разбить свои сетки на стыке и вращать их отдельно.
Отказ от ответственности: я не большой парень в 3D, поэтому я просто предложу вам математический подход, который может вам помочь.
Прежде всего, позвольте мне изложить эту маленькую схему, таким образом, мы будем уверены, что все мы говорим об одном и том же:
Синие и зеленые фигуры - оригинальные кости, полностью повернутые bone_matrix[index1]
или же bone_matrix[index2]
, Красная точка - центр вращения, оранжевая фигура - то, что вы хотите, а черная - то, что у вас есть.
Итак, вы представляете, что строите как средневзвешенное значение синего и зеленого, на этом чертеже мы видим (благодаря серым линиям), почему он так сокращается.
Вам нужно как-то компенсировать это сокращение, я бы предложил уменьшить точки от вашего центра вращения, нам нужно масштабировать значение 2 на стыке между костями и значение 1 на конечностях.
Позволять scale_matrix
быть предварительно вычисленной матрицей: масштабирование амплитуды 2 с центром в вашем центре вращения (красная точка).
Вы получите этот шейдер:
vec4 vert1 = (bone_matrix[index1]*vertex_in)*weight;
vec4 vert2 = (bone_matrix[index2]*vertex_in)*(1-weight);
vec4 inter = vert1+vert2;
vec4 scaled1 = inter*(1-2*min(weight, 1-weight));
vec4 scaled2 = (scale_matrix*inter)*(2*min(weight, 1-weight));
gl_Position = scaled1+scaled2;
Боюсь, я не могу проверить это прямо сейчас (я не знаю много о GLSL), но я думаю, что вы сможете адаптировать его к вашему случаю, если что-то не подходит.
В зависимости от вашего реального приложения вам может понравиться этот вариант: вы можете добавить дополнительную полосу между частями, например так:
Веса показаны зеленым / чирок. Однако это требует небольшого обмана костей, поэтому, когда вы наклоняетесь вправо, вы используете правую кость и устанавливаете центр вращения вправо, а когда слева - левые кости и центр вращения слева.