Расчет скелетной анимации в шейдере
Здравствуйте дорогие читатели!
Извиняюсь за задержку со статьей. Сегодня будем продолжать знакомство с 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. С этим оределились. Теперь надо определиться как нам трансформировать вершины в шейдере. Есть два пути решения:
- Конвертировать кватернион в матрицу и ею трансформировать вершину
- Трансформировать вершину непосредственно кватернионом
Как по мне второй вариант более предпочтительней так как подразумевает меньшее количество операций. Код трансформации вектора с помощью кватерниона я взял из 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 (4613)
Спасибо за материал, очень помог
Всегда рад помочь.
Отличная статья. За переход от матриц к кватернионам отдельное спасибо - как раз искал подобное.
Очень рад что вам пригодилась статья. Будем и дальше стараться помочь
Каждый раз как вы расписываете новую фичу БЕЗ поддержки реализации на древнем оборудовании
- вы сугубо пиарите новые девайсины,
а ведь разрабам игр полезно поддерживать и совместимость со старым 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 !
Здравствуйте!
Статья хорошая, практически единственная в своем роде на русском языке, огромное спасибо.
Такой вопрос: изучение трансформации вида кватернион+вектор Вы оставили на самостоятельное изучение, отправив изучать код. И все бы ничего, код читается замечательно, однако в нужном месте не поставлен нужный комментарий: мне непонятен физический смысл функции Transform::Mod2() (MathLib/MathLib.h, 452 строка). Судя по функции Bone::AnimateHierarhy() (Skeleton.h, 96 строка) Transform::Mod() используется для применения родительских трансформаций, а Transform::Mod2 используется для применения трансформации первого кадра. Но в статье нет упоминания о том, чем различаются эти два вида трансформаций. Было бы здорово, если бы Вы пояснили это в комментарии.