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

 

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

Извиняюсь за задержку со статьей. Сегодня будем продолжать знакомство с 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;
}

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

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

Исходный код к статье:
SkinnedMeshInShader.zip (4606)

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

  1. Спасибо за материал, очень помог

  2. Отличная статья. За переход от матриц к кватернионам отдельное спасибо - как раз искал подобное.

  3. Каждый раз как вы расписываете новую фичу БЕЗ поддержки реализации на древнем оборудовании
    - вы сугубо пиарите новые девайсины,
    а ведь разрабам игр полезно поддерживать и совместимость со старым HW, (что не так уж и сложно!),
    начиная с древнего вроде моего второго ПК iP2-350(т.е.даже без всяких там SSE) NVGF2MX400 128 XP/98 который жалко выкидывать, на котором можно чтото поделать:) пока основной занят, да и у многих других - не самые топовые, ибо каждый год покупать новый комп не очень умно.

    • Здравствуйте!

      Поймите, что игроделам важно охватывать целевую аудиторию, ту которой большинство. Т.к. на данный момень >60% всех игровых компов имеют поддержку SSE2 и SM3.0 поддержка столь устаревших конфигураций просто не рентабельна.
      Мне тоже очень хочется вспомнить молодость, когда оптимизировали кон на ассемблере считаю латентность каждой пары команда-регистр по докам Интела, но эта романтика давно в прошлом - быстро и как можно дешевле сделал софт и продавай - вот как живут сейчас.

      • > "Поймите, что игроделам важно охватывать целевую аудиторию, ту которой большинство"

        У нормальных игроделов - целевая аудитория не просто большинство, а максимум её.
        Тупо из-за лени/предрассудков программиста не реализовавшего все варианты оборудования - равно отказываться от любого дополнительного дохода. Даже например 1% покупателей - всё равно ещё как покроет затраты на совместимость притом уменьшив число притензий покупателей на несовместимость или тормоза, что даже куда дороже.

        Конечно, программист в компаниях - делает как ему побыстрей и похалтурней, это его личного руководителя - сфера компетнтности...

        Абсолютно все совремнные игры мог ли бы работать вовсе не только на около-топовых конфигурациях, с пониженым качеством соответвующей её! (в ч.н.даже выше указанной, хоть в NF GF2MX вообзще нет шейдеров, по крайне мере есть акселерация прочего 3D рендера)
        Но, это надо было сразу закладывать даже не оптимизацию, а просто поддержку такого. Притом естественно это привело бы и к большей производительности на максимальном качестве иначе - ещё большее качество его. Но, как сказал это руководителя - сфера компетнтности...
        Простой пример: раньше с целью обратной совместимость в 3D Action играх даже делали возможность уменьшения окна вывода! Сейчас же ...хорошо если вообще есть Найстроки, хотя бы с видеорежимом и выключением самых тяжолых и вторичных графических для gameplay, некритичных, улучшателей типа AA и далее.

        Впрочем, безусловно давно наблюдается и тенденция сговора с производителями оборудования и ОС (MS,Intel и прч,NVidia), так же как и у программо-делов. Те же авторы 3D Max как до даже проболтались о скрытом договоре своего руководства с MS: что мол они не в состоянии физчески обеспечить совместимость с плагинами для предыдущих версий своего продукта, т.к.они - обязанны компилировать его только более новой версий MS компилятора.
        (намеренно сделанной в каждой следующей версией - не только более тормозной(и особенно на предыдуем поколении оборудовании), не только по возможности несовместимой с ранее проданными их же ОСями, но и просто не совместимые по внутреннему(mangled) именованию линкуемых SDL плагинам функций, с целью несовместимости с своим же предыдущим комипилятором - для покупки плагиностроителями и не только нового компилятора, к нему ОС, к той - всего компьютера.

    • Сделай тетрис. Шейдеры лет 10 назад как уже существуют.
      "Новая фича" - это скелетная анимация, которая появилась в Half-Life в 98-м году ?
      Конечно, несложно под старое железо это дело переписать. Вот только тут конкретной реализации нет и скопистить не удасться, а прийдется мозг включать. А вот это уже очень печально, да.
      "каждый год покупать новый комп не очень умно" - AMD, Intel, NVidia с вами не согласятся.

      • Ну вы и туп, не даром ник - ololo...

        Совремнные "тетрисы" - не менее графически оформленные всего прочего, а порой и лучше.

        Фича же - использование SSE / шейдеров / OGL только 3.0

        А, даже просто о интегрированный видеокартах слышали и их ограниченном функционале и тем более тормозах, тем более при обработке шейдеров? А, о интегрированных лаптопных?...
        А, о том что даже тот же SSE - патентованная технология и не обязательно прям во всех процессорах обязан быть.
        Таки скажу, ololo !

  4. Здравствуйте!

    Статья хорошая, практически единственная в своем роде на русском языке, огромное спасибо.
    Такой вопрос: изучение трансформации вида кватернион+вектор Вы оставили на самостоятельное изучение, отправив изучать код. И все бы ничего, код читается замечательно, однако в нужном месте не поставлен нужный комментарий: мне непонятен физический смысл функции Transform::Mod2() (MathLib/MathLib.h, 452 строка). Судя по функции Bone::AnimateHierarhy() (Skeleton.h, 96 строка) Transform::Mod() используется для применения родительских трансформаций, а Transform::Mod2 используется для применения трансформации первого кадра. Но в статье нет упоминания о том, чем различаются эти два вида трансформаций. Было бы здорово, если бы Вы пояснили это в комментарии.

 Оставить комментарий

(required)

(required)

Вы можете использовать HTML теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

© 2011 3D-Orange.com.ua
e-mail me

3D-Orange.com.ua is proudly powered by WordPress.
Suffusion theme by Sayontan Sinha