javascript

Шейдер меха на WebGL 2

  • суббота, 25 марта 2017 г. в 03:13:42
https://habrahabr.ru/post/324728/
  • Разработка под Android
  • WebGL
  • JavaScript


Идея


Новый стандарт WebGL 2 стал недавно доступен в последних версиях Firefox и Chrome, так что возникло желание опробовать некоторые новые возможности. Одна из наиболее полезных и востребованных функций WebGL 2 (и OpenGL ES 3.0, на котором он основан) это дублирование геометрии (англ. instanced rendering). Эта фича позволяет уменьшить количество вызовов отрисовки (draw calls) путем многократной отрисовки одной и той же геометрии с измененными параметрами. Эта функция присутствовала и в некоторых реализациях WebGL 1, но требовала наличия определенного расширения. Наиболее часто эта функция применяется для создания систем частиц и растительности, но также довольно часто она используется для симуляции меха.


Концепт и демо


Есть довольно много различных подходов в симуляции меха в OpenGL, но данная реализация основана на методике, описанной в этом видео-уроке. Несмотря на то что в нем описано создание шейдера для Unity, подробные и наглядные пошаговые инструкции из этого видео были взяты за основу создания OpenGL ES шейдера с нуля. Если вы не знакомы с общими принципами симуляции меха, то рекомендуем потратить 13 минут на просмотр этого видео для понимания общих принципов его работы.

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

Просмотреть готовую демку можно здесь. Если браузер не поддерживает WebGL 2 (например, на данный момент мобильные браузеры поддерживают только WebGL 1), то вот видео демки:

Чтобы наглядно показать как работает симуляция меха, начнем с рендеринга двух дополнительных слоев меха с достаточно большой толщиной (расстоянием между слоями). На следующем изображении видно исходный объект без меха и два полупрозрачных слоя поверх него:


Увеличивая количество слоев и уменьшая толщину слоя мы постепенно получаем более реалистичный результат. На этом изображении из 6 относительно плотных слоев уже хорошо видно постепенное уменьшение прозрачности слоев от полностью непрозрачного до полностью прозрачного:


И достаточно реалистичный конечный результат с использованием 20 очень тонких слоев:


В нашей демке используется 5 различных предустановленных параметров — 4 меха и один мох. Все они рендерятся одними и теми же шейдерами но с различными заданными параметрами.

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

Далее начинается отрисовка слоев меха. Они прозрачные и потому требуют подбора корректного режима смешивания цветов для того, чтобы выглядеть реалистично. Использование обычной glBlendFunc() приводило к либо чересчур ярким, либо же слишком тусклым цветам меха, так как этот режим влияет на альфа канал и таким образом искажает цвета. Функция glBlendFuncSeparate() позволяет задавать различные режимы смешивания цвета для RGB и альфа-каналов фрагментов и используя ее удалось сохранить альфа-канал без изменений (он полностью контролируется шейдером) и в то же время корректно смешивать цвет каждого слоя меха с самим собой и другой геометрией.

В демке используется следующий режим смешивания цветов:

gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE);

Примеры попыток подбора режима смешивания:


После того как задан правильный режим смешивания цветов, можно приступать к собственно отрисовке меха. Отрисовка всего меха реализована одним вызовом — вся работа по дублированию геометрии выполняется в одном шейдере. Видеокарта без участия драйверов повторяет отрисовку геометрии заданное количество раз и, следовательно, нет затрат на дополнительные вызовы OpenGL команд. Все последующие разъяснения относятся только к этому шейдеру. Синтаксис GLSL 3.0, используемый в WebGL 2 и OpenGL ES 3.0, несколько отличается от GLSL 1.0 — можно ознакомиться с различиями и инструкциями по портированию старых шейдеров здесь.

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

float f = float(gl_InstanceID + 1) * layerThickness; // calculate final layer offset distance
vec4 vertex = rm_Vertex + vec4(rm_Normal, 0.0) * vec4(f, f, f, 0.0); // move vertex in direction of normal

Для того, чтобы мех выглядел реалистично, он должен быть более плотным в основании и постепенно сужаться на кончиках. Этот эффект достигается путем постепенного изменения прозрачности слоев. Также, для симуляции ambient occlusion мех должен быть темнее у основания и светлее на поверхности. Типичными параметрами для меха является начальный цвет [0.0, 0.0, 0.0, 1.0] и конечный цвет [1.0, 1.0, 1.0, 0.0]. Таким образом мех начинается полностью черным и заканчивается цветом диффузной текстуры, в то время как прозрачность увеличивается от полностью непрозрачного слоя до полностью прозрачного.

Сперва в вертексном шейдере вычисляется коэффициент цвета и на основании этого коэффициента интерполируется цвет между начальным и конечным. В фрагментном шейдере этот цвет множится на цвет диффузной текстуры. Конечным этапом является умножение альфа канала фрагмента на цвет из черно-белой текстуры, которая задает распределение шерсти.

// vertex shader
float layerCoeff = float(gl_InstanceID) / layersCount;
vAO = mix(colorStart, colorEnd, layerCoeff);

// fragment shader
vec4 diffuseColor = texture(diffuseMap, vTexCoord0); // get diffuse color
float alphaColor = texture(alphaMap, vTexCoord0).r; // get alpha from alpha map
fragColor = diffuseColor * vAO; // simulate AO
fragColor.a *= alphaColor; // apply alpha mask

Возможно много различных вариантов реализации колышущегося меха на ветру. В нашей демке мы немного смещаем каждую вершину на основании циклически изменяемого параметра, переданного в шейдер. Для того чтобы все слои смещались синхронно, необходимо вычислить некое уникальное значение для каждой вершины с одинаковыми координатами. В данном случае не получится использовать встроенную переменную gl_VertexID, так как ее значение различается для разных вершин, даже для тех у которых координаты одинаковы. Так что мы вычисляем некоторую “магическую сумму” из координат вершины и используем ее в синусоидальной функции для создания “волн” ветра. Пример смещения вершин на основании значения параметра time:

const float PI2 = 6.2831852; // Pi * 2 for sine wave calculation
const float RANDOM_COEFF_1 = 0.1376; // just some random float
float timePi2 = time * PI2;
vertex.x += sin(timePi2 + ((rm_Vertex.x+rm_Vertex.y+rm_Vertex.z) * RANDOM_COEFF_1)) * waveScaleFinal;
vertex.y += cos(timePi2 + ((rm_Vertex.x-rm_Vertex.y+rm_Vertex.z) * RANDOM_COEFF_2)) * waveScaleFinal;
vertex.z += sin(timePi2 + ((rm_Vertex.x+rm_Vertex.y-rm_Vertex.z) * RANDOM_COEFF_3)) * waveScaleFinal;

Дальнейшие улучшения


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

Можете брать код с Гитхаба, использовать и улучшать его в своих проектах — он использует лицензию MIT.