root@home‎ > ‎

Расчет скелетной анимации в шейдере

Здравствуйте дорогие читатели!

Извиняюсь за задержку со статьей. Сегодня будем продолжать знакомство с OpenGl 3.0 а также переносить расчет скелетной анимации в шейдер. В предыдущей статье (3DS Max - Экспорт скелетной анимации) мы с Вами  написали плагин экспорта мэшей со скелетной анимацией а также тестовый просмотрщик этой анимации. Сегодня мы немного модифицируем просмотрщик так, чтобы конечный расчет анимации (применение «взвешенной» трансформации костей на вершины) производился в шейдере.

 

Итак, кратко вспомним как у нас работает анимация сейчас.

Создается VBO с типом использования GL_STREAM_DRAW.

Далее в каждый момент времени рассчитываются трансформации костей, после этого обновляется иерархия костей.

Затем мы идем циклом по всем вершинам мэша и каждую вершину трансформируем действующими на нее костями.

После чего уже трансформированные вершины заливаются в VBO и отрисовываются.

Псевдокод:

foreach vertex in vertexArray

            foreach weight in vertex.weights

                        vertex.pos += transforms[weight.boneID].Transform(srcVert.pos) * weight.weight

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

Конечно можно оптимизировать это дело переписав на SSE. Однако выигрыш будет не особо впечатлительный. Да и проблема с заливкой данных в VBO остается.

 Да зачем мучаться если под боком есть отдельная мультипроцессорная, многопоточная вещица заточенная под обработку вершин! Да, я говорю об видеокарте! Такие расчеты современные видеокарты щелкают как семечки, обрабатывая за раз вершины пачками. К тому же мы избавляемся от необходимости каждый раз заливать вершины в VBO – один раз залили и затем просто передаем трансформации костей в шейдер.

В принцыпе просто перенести анимацию в вершинный шейдер не сложно. Просто передаем посчитанные матрицы костей шейдер и там уже трансформируем вершины. Однако каждая матрица это 16 floats, а ведь нам реально нужен только поворот и смещение, тоесть кватернион + вектор = 7 floats, что более чем в 2 раза меньше, а значит мы сможем передавать трансформации в шейдер быстрее. Ну или передавать их больше. Ну и последний плюсик – нам больше не нужно будет тратить время на перевод кватерниона в матрицу.

Итак, приступим. Так как экспортер записывает анимацию уже в виде кватернион+вектор, то его мы трогать не будем. Для начала нам надо отучить нашу программу от матриц и научить пользоваться кватернионами. Для этого введем новый класс Transform

class Transform

{

public:

      quat  q;

      vec3  v;

};

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

Теперь надо подумать об том как передавать индексы костей и веса в шейдер. Конечно же мы будем передавать их в вершинных атрибутах, но количество влияющих на вершину костей у нас варьируется. Значит пришло время вводить ограничения на количество влияющих костей. Не будем гадать и поставим ограничение в 4 кости на вершину – стандарт для всех современных движков. Но наша модель содержит больше костей на вершину, что же делать? Если Вы работаете в команде разработчиков, значит Вы можете оговорить это ограничение с 3D-артистами. Мы же пойдем другим путем – просто отбросим менее значимые кости (с наименьшим весом) и нормализуем оставшиеся веса, чтобы они в сумме давали единицу.

std::sort( weights.begin(), weights.end(), WeightComp );

accumInv = 0.0f;

for ( j = 0; j < limit; ++j )

      accumInv += weights[j].weigth;

accumInv = 1.0f / accumInv;

for ( j = 0; j < limit; ++j )

      weights[j].weigth *= accumInv;

vertexes->numWeights = limit;

Также если количество влияющих костей у вершины меньше 4-х, то не задействованные веса просто обнуляются, но все равно передаются и считаются. По моему субъективному мнению лишняя трансформация лучше чем цикл в шейдере.

Ok. С этим оределились. Теперь надо определиться как нам трансформировать вершины в шейдере. Есть два пути решения:

1)      Конвертировать кватернион в матрицу и ею трансформировать вершину

2)      Трансформировать вершину непосредственно кватернионом

Как по мне второй вариант более предпочтительней так как подразумевает меньшее количество операций. Код трансформации вектора с помощью кватерниона я взял из DooM 3 SDK. Конечный шейдер выглядит так:

#version 130
precision highp float;

// iOrange - uniforms
uniform mat4x4 modelViewProjection;
uniform float bones[126];

// iOrange - incoming params
in vec4 vPos;
in vec4 vIndexes;
in vec4 vWeights;
in vec2 vTexCoord;

// iOrange - outgoing params
out vec2 tc;

vec3 VertexTransform(vec3 p, int index)
{
    int i = index * 7;

    // restore rotation component (quaternion)
    float x = bones[i];
    float y = bones[i+1];
    float z = bones[i+2];
    float w = bones[i+3];

    // restore offset component (vec3)
    float tx = bones[i+4];
    float ty = bones[i+5];
    float tz = bones[i+6];

    // original code from DooM 3 SDK
    float xxzz = x*x - z*z;
    float wwyy = w*w - y*y;
    float xw2 = x*w*2.0;
    float xy2 = x*y*2.0;
    float xz2 = x*z*2.0;
    float yw2 = y*w*2.0;
    float yz2 = y*z*2.0;
    float zw2 = z*w*2.0;
    vec3 ret = vec3((xxzz + wwyy)*p.x + (xy2 + zw2)*p.y       + (xz2 - yw2)*p.z,
                    (xy2 - zw2)*p.x   + (y*y+w*w-x*x-z*z)*p.y + (yz2 + xw2)*p.z,
                    (xz2 + yw2)*p.x   + (yz2 - xw2)*p.y       + (wwyy - xxzz)*p.z);

    return ret + vec3(tx, ty, tz);
}

void main()
{
    tc = vTexCoord;
    vec4 p = vec4(0.0, 0.0, 0.0, 1.0);
    p.xyz = VertexTransform(vPos.xyz, int(vIndexes.x)) * vWeights.x +
            VertexTransform(vPos.xyz, int(vIndexes.y)) * vWeights.y +
            VertexTransform(vPos.xyz, int(vIndexes.z)) * vWeights.z +
            VertexTransform(vPos.xyz, int(vIndexes.w)) * vWeights.w;
    gl_Position = modelViewProjection * p;
}

Как обычно картинка для демонстрации результата:


Вот в принципе и все. Всем спасибо за внимание.

С ув. =[0r@ngE]= (iOrange)

ċ
SkinnedMesh.zip
(695k)
Sergey iOrange,
Jan 10, 2010, 10:56 AM
Comments