Как перенести шейдер из игрового движка в Substance Painter

Меня зовут Тарас Улейский, я Technical Artist в Plarium Kharkiv. Для оптимизации графики нашей Survival RPG на мобильных устройствах мы использовали свои кастомные шейдеры. Они предполагают использование уникальных текстур и карт, которые не похожи на текстуры и карты в других популярных способах шейдинга. В результате 3D-художникам не совсем понятно, как создавать эти текстуры для ассетов в игре. Чтобы сразу можно было увидеть, как 3D-модель будет выглядеть в движке игры на этапе текстурирования, я перенес шейдер в Substance Painter. Материалов по API в Substance Painter на данный момент практически нет, я изучил эту тему самостоятельно, поэтому решил поделиться своими наработками.

tthiz2dpzs2v-tdmzga9nskoeao.png

Шейдер в юнити


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

wdmtfrjwwe6gach03sv2-u31cya.png

На примере ниже показано, как реализован Matcap в шейдерграфе. В данном случае две Matcap-текстуры упакованы в одну и разбиты по каналам. То есть металл и неметалл в каналах R и G соответственно.

gttnx1bhczfa3fuqiwmgrqegdvk.jpeg

Интерполируются два Matcap’а для примера по чекеру.

ojgorowglvxcnfsbqns551jwz3e.gif

В итоге получается некая аналогия с металлом и неметаллом как в PBR-шейдинге.

Нам хотелось добавить в материалы шероховатостей и грязи, создать некий аналог roughness в PBR-шейдинге. Для этого мы воспользовались методом текстурирования mip-mapping. Последовательностью текстур создается так называемая MIP-пирамида с разрешением от максимального до 1×1. Например: 1×1, 2×2, 4×4, 8×8, 16×16, 32×32, 64×64, 128×128. Каждая из этих текстур называется MIP level. Чтобы реализовать потертости в шейдере попиксельно, основываясь на маске, нужно выбрать требуемый MIP level. Получается так: там, где пиксель на маске черный, на Matcap«е выбирается максимальный MIP level, а там, где цвет пикселя белый, MIP level равен 0.

r933bw4z7ja0uwi4o8uwoomrthe.gif

7ok7n9zj0oxq3ouidw_kwt9ezww.jpeg

В итоге шейдер дает возможность имитировать отражения и блики, добавлять легкие шероховатости и потертости. И это всё без использования Cubemap, без сложных расчетов освещения и других техник, которые значительно снижают производительность мобильных устройств.

gayrur5ty9hr3xdfl56duhlcgqi.gif

Настройка Substance Painter для создания шейдера


Все доступные шейдеры в Substance Painter написаны на языке GLSL.
Конкретно для написания шейдера под Substance Painter я использую бесплатный VS Code. Для подсветки синтаксиса лучше использовать расширение Shader languages support for VS Code.

fzxoiwxrllvqgwvfammn5uuq7b8.png

Об API в Substance Painter материалов очень мало, поэтому стандартная документация, которую можно найти в Help/Documentation/Shader API, просто бесценна.

2trwyxswqga9eozdwbk4mmc1ztk.png

Второе, что будет помогать в написании шейдера, — стандартные шейдеры в Substance Painter. Чтобы их найти, перейдите в …/Allegorithmic/SubstancePainter/resources/shelf/allegorithmic/shaders.

Давайте попробуем написать самый простой unlit-шейдер, который будет показывать Base color. Для начала создадим текстовый файл с расширением .glsl и напишем такой вот несложный шейдер. Возможно, пока что ничего не понятно, я расскажу детальнее о структуре шейдера в Substance Painter дальше.

mkckrvh6j8r7fz7zh72sjfhylgg.png

Создайте новый проект и перетяните шейдер на ваш shell. В выпадающем списке Import your resources to выберите project «имя_проекта».

a0us24ykiapgfc-ioeuxt7mpckc.png

Это нужно, чтобы можно было обновлять все изменения.

Теперь перейдите в Window/Views/Shader Settings и в появившемся окне выберите ваш новый шейдер. Можно воспользоваться поиском.

9ubt6padacui5ccb77bgpbywfcg.png

Если вы увидите, что вся модель белая и по ней можно рисовать Base color, значит, вы всё сделали правильно. Теперь можно сохранить проект и перейти к следующему разделу.

jzigwtwpb73uz6s8paesepkcqao.gif

Если модель будет розового цвета, то, скорее всего, в шейдере ошибка — уведомление об этом будет в консоли.

Построение шейдера в Substance Painter


Рассмотрим структуру шейдера на примере ранее описанного unlit-шейдера.

mkckrvh6j8r7fz7zh72sjfhylgg.png

Метод shade — это базовая часть шейдера, без него он работать не будет. Всё, что будет описано внутри, можно отобразить на 3D-модели. Все конечные расчеты выводятся через функцию diffuseShadingOutput ().

Строки 3 и 4 создают параметр и переменную соответственно. Параметр связывает канал Base color с переменной, в которой будет храниться нарисованная текстура. Все параметры прописаны в справке, в случае с Base color всё должно быть прописано так, как в примере. Строка 8 раскладывает текстуру по uv-координатам 3D-модели. Отмечу, что для текстуры с Base color используется система Sparse Virtual Textures, потому первой строкой подключается библиотека lib-sparce.glsl.

Можно найти множество реализаций Matcap«a, но его основная суть в том, что нормали модели направляются в сторону камеры и по осям x и y разворачивается текстура. Чтобы повернуть нормали в сторону камеры, нам нужна view matrix, или матрица вида. Найти такую можно в справке, о которой упоминалось выше.

b594oxz6c9doiz0sdm2jbzxt6ow.png

Итак, это такие же задекларированные названия, как и в случае с Base color. Теперь нам нужно получить нормали 3D-модели.

wgos4bygzxftd_sbadht0lqznhy.png
Ноль как четвертый элемент вектора обязателен.

Перемножение матрицы вида с вектором нормали развернет нормаль к камере.

tybtc0ibyb9ybigfr5x-tse61ne.png

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

Теперь можно из viewNormal создать uv-координаты.

94efjnpw9ingufg9ocbgfdxxnwq.png

Пришло время подключить Matcap-текстуру.

edk3k848a9wdmfa06dunpn8zrni.png

В данном случае параметр создаст в интерфейсе шейдера поле для текстуры, и если в проекте будет текстура с именем «Matcap_mip», то Substance Painter автоматически подтянет ее.

ylq5e0hbag9jh4bpgj2ro2vot4e.png

Проверим, что получилось.

rezbiydl_fat8ornozz67bwjbs0.png

Тут текстура Matcap’а раскладывается по новым координатам и на выходе перемножается с Base color. Хочу обратить внимание на то, что текстура Matcap раскладывается через функцию texture (), а Base color — через функцию textureSparse (). Это происходит потому, что текстуры, заданные через интерфейс шейдера, не могут иметь тип SamplerSparse.

Результат должен выглядеть примерно так:

l7v1rqhspgrp92iuffebw2-jmua.gif

Теперь добавим маску, которая будет смешивать два Matcap’а. Для удобства добавим два Matcap’а в одну текстуру, разбив их по каналам. В итоге две Matcap-текстуры будут в каналах R и G соответственно.

Получится что-то вроде этого:

a-nrih00ttqwnk6ewdv-pbhq9yo.jpeg

Приступим к добавлению маски в шейдер. Принцип схож с добавлением Base color.

ttlvjfnodtzhu_qp_mpq-oqfpga.png

Достаточно заменить в параметре значение basecolor на user0.

Теперь достанем значение маски в пиксельном шейдере и смешаем текстуры Matcap.

pa42srwn-29afh50di07sepap_w.png

Здесь в маске используется только канал R, потому что она будет черно-белая. Два канала matcap смешиваются при помощи функции mix () — аналог lerp в Unity.

Давайте обновим шейдер и добавим кастомные каналы в интерфейсе. Для этого нужно перейти в Window/Views/Texture Set Settings, в окне возле заголовка Channels кликнуть на плюс и выбрать из большого списка user0.

0tv0mo6ovcvfztcawlrhbzrhzbs.png

Канал можно назвать как угодно.

Теперь, рисуя по этому каналу, можно увидеть, как смешиваются две Matcap-текстуры.

rijqnu1-0utez7t0babbdv11--s.gif

В шейдере для Unity использовались еще и карты нормалей для Matcap, которые запекались с высокополигональной модели. Попробуем сделать в Substance Painter то же самое.

Чтобы использовать все операции над нормалями, нужно подключить соответствующую библиотеку:

6fzzz36x9epa7a34gew0hcq-fro.png

Теперь подключим карты нормалей. В Substance Painter их две: одна получается путем запекания, а по второй можно рисовать.

hw7q86rpk52sdkyygjo32vufv5s.png

По параметрам можно догадаться, что channel_normal — это карта нормалей, по которой можно рисовать, а texture_normal — запеченная карта нормалей. Отмечу еще, что имя переменной texture_normal вшито в API и назвать ее по своему усмотрению нельзя.

Дальше распаковываем карты в пиксельном шейдере:

yqwxsvn2ohvouv-rwzui9blzp6q.png

Затем смешиваем карты нормалей и нормали, которые находятся на вертексах модели. Для этого в библиотеке, подключенной выше, есть функция normalBlend ().

2yr0zsobknqtlz7ooj_6xd4onsg.png

Смешиваем сначала две карты нормалей, а потом нормали модели. Хотя на самом деле неважно, в каком порядке их смешивать.

Поворот нормалей в направлении взгляда камеры будет выглядеть так:

ycblmti7x5nhbfhdlx6yyrqyl4o.png

Дальше можно ничего не менять, всё останется так же. Должно получиться как-то так:

gmnyza2j-simray-86frype0usa.gif

Mip-mapping, как упоминалось выше, в данном случае нужен для имитации потертостей, что-то наподобие карты roughness в PBR-шейдинге. Но основная проблема в том, что пирамида из mip-карт не генерируется для текстуры, которая передается из интерфейса шейдера, и соответственно метод textureLod () из glsl работать не будет. Можно было бы пойти другим путем и загрузить текстуру Matcap’а через user channel, как это делалось для смешивания Matcap«ов. Но тогда качество текстуры сильно снизится и появятся странные артефакты.

Альтернативное решение — создать пирамиду MIP-карт вручную, в Adobe Photoshop или другом подобном редакторе, а потом выбирать MIP level. Пирамида строится достаточно просто. Нужно исходить из размера оригинальной текстуры — в моем случае это 256×256. Создаем файл размером 384×256 (384, потому что 256+256/2) и теперь уменьшаем оригинальную текстуру в два раза до тех пор, пока она не будет размером в один пиксель. Все версии уменьшенных текстур размещаем справа от оригинальной текстуры в порядке возрастания. Должно получиться вот так:

sgh-qwq1kmwq_gh7hgkg1l8fnxq.jpeg

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

Проще всего хранить uv-координаты, которые будут рассчитываться для каждой текстуры, в массиве. Размер массива будет определяться как log2(height). Нам нужны оригинальные uv, потому добавим их в аргумент функции. Чтобы определить, какой элемент массива использовать на конкретном пикселе, добавим level в аргумент функции.

hqoxumpnar903hkoghv3hgtfzr4.png

Теперь рассчитаем uv для оригинальной текстуры, то есть обрежем те лишние 128 пикселей по ширине. Для этого достаточно x координату умножить на ⅔.

hoornhxxr9w2yj_bgr8ckt9rdw8.png

Чтобы использовать остальные текстуры из пирамиды, нужно найти закономерности. Когда мы создавали пирамиду из текстур, то можно было заметить, что каждый раз текстура уменьшается в два раза от предыдущего размера. То есть, во сколько раз уменьшается размер текстуры, можно определить, возводя 2 в степень MIP level.

9iyk6k9udtbo2gyundt8erj8a0q.png

Получается, если выбрать level, например, 4, то текстура уменьшится в 16 раз. Так как uv-координаты определяются от 0 до 1, то размер нужно нормализовать, то есть 1 разделить на то, во сколько раз уменьшилась текстура, например, 1 разделить на 16.

Используя полученное значение переменной size, можно высчитать координаты для конкретного MIP level.

wecvkhragapkffyxyussvrbgkes.png

Размер uv уменьшается так же, как и размер текстуры. По координате x текстура всегда сдвигается на ⅔. Сдвиг по координате y можно определить как сумму всех значений переменной size для каждого значения level. То есть если значение level=1, то uv по координате y сдвинутся на 0 пикселей, а если level=2, то сдвиг будет половиной от высоты текстуры — 128 пикселей. Если level=3, то сдвиг получится как 128+64 пикселя и так далее. Сумму всех сдвигов можно получить с помощью цикла.

njflwi83bfg42lecjugixw2xrr8.png

Теперь каждую итерацию переменная offset будет суммироваться и сдвигать текстуру по оси y на нужное количество пикселей. Пошагово алгоритм выглядит примерно так:

jccdllnuvqh288742jsx21jtbme.gif

Последним шагом нужно вывести канал, который будет выбирать нужный level на каждом пикселе. Такое мы уже делали, ничего нового.

nosh4drvum-rq3fif1mh2qdif_0.png

vg-5w0rym_gpaygfgzhtkubfxly.png

Чтобы текстурой выбирать MIP level, достаточно на текстуру умножить длину массива. Теперь можно подключать новые uv-координаты через написанный только что метод.

af-h1agpkidgfd7153wgsfgltaa.png

Не забываем текстуру перевести в тип int, так как это теперь индекс для массива.
Далее нужно в Substance Painter добавить кастомный канал, как это мы делали раньше. Должно получиться так:

qc4ph4jzivfdrtqd5w4dqtn0ojw.gif

mps41cvu0dlu_kqkkv7qkrtmb5m.gif

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

xyg_y1vvg_rhtoxwuepokzrp9x0.png

ny17dfcnyibmvgvfx8pbphsxkom.png

Разместим произвольно источник света и умножим позицию на матрицу вращения.

bogifpq90um2ygho-bgv0zdvy7a.png

Теперь источник света будет вращаться вокруг оси y по нажатию shift, но пока что это всего лишь вектор, в котором хранится позиция источника света. Есть хороший материал о том, как имплементировать направленный свет в шейдере. Будем ориентироваться на него. Нам осталось определить направление света и освещенность нашей модели.

oka4dfn33tv1t2meohooiu9fz2k.png

Цвет тени и цвет источника света будут задаваться параметрами:

mrzskmiirmqmddlhwwg59ab_mpm.png

Параметры цвета интерполируются по рассчитанной выше освещенности.

c4ci8hm54ul19f-lg92oox2t7li.png

Получится вот так:

dprxs9gy7_jaw4p3lu_ux7a9xbi.gif

С помощью этих параметров можно регулировать цвет тени и цвет источника света через интерфейс Substance Painter.

wso8e3nnmdky15efibtajhj0rya.gif

Создание и настройка пресета


Когда шейдер готов, нужно импортировать текстуру Matcap и шейдер с настройкой shelf.

hgw2wh4tdzd6i1l4hgcpi8iebpk.png

Удаляем все неиспользуемые каналы и добавляем user channels:

hlqsxepohrs1j0hbrlkeghgch5g.png

Пресет для экспорта текстур будет выглядеть как и любой другой, за исключением того, что в нем будут использованы наши кастомные каналы.

zpxf7oje5map2cl0gurvpustk7u.png

Создадим шаблон всех настроек, чтобы при создании проекта сразу был назначен нужный шейдер и настроены все каналы текстур. Для этого переходим в File/SaveAsTemplate и сохраняем шаблон.

pbxnaqjvwraborjnospf8rglc10.png

Теперь при создании нового проекта не нужно ничего настраивать — достаточно выбрать нужный темплейт.

hhxwbyfzcemnynsqglcabzbgzto.png

Что получили


Технический художник может создавать спецэффекты, настраивать сцены и оптимизировать процессы рендеринга. Также я стремился, чтобы модели брони и оружия в игре Stormfall: Saga of Survival были именно такими, какими их задумывали 3D-художники. В результате 3D-модель в Substance Painter выглядит так же, как в игровом движке.

x_icqrlq2s3_osfrps5c-0qttwo.gif
3D-модель в Substance Painter с кастомным шейдингом.

7e79sgwszeyazecwzi18j2fhv4s.gif
3D-модель в Unity с кастомным шейдингом.

Надеюсь, статья была полезной и вдохновила вас на новые свершения!

© Habrahabr.ru