Меня зовут Тарас Улейский, я Technical Artist в Plarium Kharkiv. Для оптимизации графики нашей Survival RPG на мобильных устройствах мы использовали свои кастомные шейдеры. Они предполагают использование уникальных текстур и карт, которые не похожи на текстуры и карты в других популярных способах шейдинга. В результате 3D-художникам не совсем понятно, как создавать эти текстуры для ассетов в игре. Чтобы сразу можно было увидеть, как 3D-модель будет выглядеть в движке игры на этапе текстурирования, я перенес шейдер в Substance Painter. Материалов по API в Substance Painter на данный момент практически нет, я изучил эту тему самостоятельно, поэтому решил поделиться своими наработками.
![](https://habrastorage.org/webt/tt/hi/z2/tthiz2dpzs2v-tdmzga9nskoeao.png)
В игре используется Matcap-шейдинг. Помимо обычной диффуз-текстуры, в шейдер еще передается две заранее созданные Matcap-текстуры. Они интерполируются и размываются с помощью двух масок соответственно. В итоге Matcap-текстура умножается на диффузную и на материале можно увидеть фейковые блики и отражения.
![](https://habrastorage.org/webt/wd/mt/fr/wdmtfrjwwe6gach03sv2-u31cya.png)
На примере ниже показано, как реализован Matcap в шейдерграфе. В данном случае две Matcap-текстуры упакованы в одну и разбиты по каналам. То есть металл и неметалл в каналах R и G соответственно.
![](https://habrastorage.org/webt/gt/tn/x1/gttnx1bhczfa3fuqiwmgrqegdvk.jpeg)
Интерполируются два Matcap'а для примера по чекеру.
![](https://habrastorage.org/webt/oj/go/ro/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.
![](https://habrastorage.org/webt/r9/33/bw/r933bw4z7ja0uwi4o8uwoomrthe.gif)
![](https://habrastorage.org/webt/7o/k7/n9/7ok7n9zj0oxq3ouidw_kwt9ezww.jpeg)
В итоге шейдер дает возможность имитировать отражения и блики, добавлять легкие шероховатости и потертости. И это всё без использования Cubemap, без сложных расчетов освещения и других техник, которые значительно снижают производительность мобильных устройств.
![](https://habrastorage.org/webt/ga/yr/ur/gayrur5ty9hr3xdfl56duhlcgqi.gif)
Все доступные шейдеры в Substance Painter написаны на языке GLSL.
Конкретно для написания шейдера под Substance Painter я использую бесплатный VS Code. Для подсветки синтаксиса лучше использовать расширение Shader languages support for VS Code.
![](https://habrastorage.org/webt/fz/xo/iw/fzxoiwxrllvqgwvfammn5uuq7b8.png)
Об API в Substance Painter материалов очень мало, поэтому стандартная документация, которую можно найти в Help/Documentation/Shader API, просто бесценна.
![](https://habrastorage.org/webt/2t/rw/yx/2trwyxswqga9eozdwbk4mmc1ztk.png)
Второе, что будет помогать в написании шейдера, – стандартные шейдеры в Substance Painter. Чтобы их найти, перейдите в .../Allegorithmic/SubstancePainter/resources/shelf/allegorithmic/shaders.
Давайте попробуем написать самый простой unlit-шейдер, который будет показывать Base color. Для начала создадим текстовый файл с расширением .glsl и напишем такой вот несложный шейдер. Возможно, пока что ничего не понятно, я расскажу детальнее о структуре шейдера в Substance Painter дальше.
![](https://habrastorage.org/webt/mk/ck/rv/mkckrvh6j8r7fz7zh72sjfhylgg.png)
Создайте новый проект и перетяните шейдер на ваш shell. В выпадающем списке Import your resources to выберите project ’имя_проекта’.
![](https://habrastorage.org/webt/a0/us/24/a0us24ykiapgfc-ioeuxt7mpckc.png)
Это нужно, чтобы можно было обновлять все изменения.
Теперь перейдите в Window/Views/Shader Settings и в появившемся окне выберите ваш новый шейдер. Можно воспользоваться поиском.
![](https://habrastorage.org/webt/9u/bt/6p/9ubt6padacui5ccb77bgpbywfcg.png)
Если вы увидите, что вся модель белая и по ней можно рисовать Base color, значит, вы всё сделали правильно. Теперь можно сохранить проект и перейти к следующему разделу.
![](https://habrastorage.org/webt/jz/ig/wt/jzigwtwpb73uz6s8paesepkcqao.gif)
Если модель будет розового цвета, то, скорее всего, в шейдере ошибка – уведомление об этом будет в консоли.
Рассмотрим структуру шейдера на примере ранее описанного unlit-шейдера.
![](https://habrastorage.org/webt/mk/ck/rv/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, или матрица вида. Найти такую можно в справке, о которой упоминалось выше.
![](https://habrastorage.org/webt/b5/94/ox/b594oxz6c9doiz0sdm2jbzxt6ow.png)
Итак, это такие же задекларированные названия, как и в случае с Base color. Теперь нам нужно получить нормали 3D-модели.
![](https://habrastorage.org/webt/wg/os/4b/wgos4bygzxftd_sbadht0lqznhy.png)
Ноль как четвертый элемент вектора обязателен.
Перемножение матрицы вида с вектором нормали развернет нормаль к камере.
![](https://habrastorage.org/webt/ty/bt/c0/tybtc0ibyb9ybigfr5x-tse61ne.png)
Не стоит забывать, что при перемножении матриц важен порядок множителей. Если вы измените порядок умножения, результаты будут другими.
Теперь можно из viewNormal создать uv-координаты.
![](https://habrastorage.org/webt/94/ef/jn/94efjnpw9ingufg9ocbgfdxxnwq.png)
Пришло время подключить Matcap-текстуру.
![](https://habrastorage.org/webt/ed/k3/k8/edk3k848a9wdmfa06dunpn8zrni.png)
В данном случае параметр создаст в интерфейсе шейдера поле для текстуры, и если в проекте будет текстура с именем «Matcap_mip», то Substance Painter автоматически подтянет ее.
![](https://habrastorage.org/webt/yl/q5/e0/ylq5e0hbag9jh4bpgj2ro2vot4e.png)
Проверим, что получилось.
![](https://habrastorage.org/webt/re/zb/iy/rezbiydl_fat8ornozz67bwjbs0.png)
Тут текстура Matcap'а раскладывается по новым координатам и на выходе перемножается с Base color. Хочу обратить внимание на то, что текстура Matcap раскладывается через функцию texture(), а Base color – через функцию textureSparse(). Это происходит потому, что текстуры, заданные через интерфейс шейдера, не могут иметь тип SamplerSparse.
Результат должен выглядеть примерно так:
![](https://habrastorage.org/webt/l7/v1/rq/l7v1rqhspgrp92iuffebw2-jmua.gif)
Теперь добавим маску, которая будет смешивать два Matcap'а. Для удобства добавим два Matcap'а в одну текстуру, разбив их по каналам. В итоге две Matcap-текстуры будут в каналах R и G соответственно.
Получится что-то вроде этого:
![](https://habrastorage.org/webt/a-/nr/ih/a-nrih00ttqwnk6ewdv-pbhq9yo.jpeg)
Приступим к добавлению маски в шейдер. Принцип схож с добавлением Base color.
![](https://habrastorage.org/webt/tt/lv/jf/ttlvjfnodtzhu_qp_mpq-oqfpga.png)
Достаточно заменить в параметре значение basecolor на user0.
Теперь достанем значение маски в пиксельном шейдере и смешаем текстуры Matcap.
![](https://habrastorage.org/webt/pa/42/sr/pa42srwn-29afh50di07sepap_w.png)
Здесь в маске используется только канал R, потому что она будет черно-белая. Два канала matcap смешиваются при помощи функции mix() – аналог lerp в Unity.
Давайте обновим шейдер и добавим кастомные каналы в интерфейсе. Для этого нужно перейти в Window/Views/Texture Set Settings, в окне возле заголовка Channels кликнуть на плюс и выбрать из большого списка user0.
![](https://habrastorage.org/webt/0t/v0/mo/0tv0mo6ovcvfztcawlrhbzrhzbs.png)
Канал можно назвать как угодно.
Теперь, рисуя по этому каналу, можно увидеть, как смешиваются две Matcap-текстуры.
![](https://habrastorage.org/webt/ri/jq/nu/rijqnu1-0utez7t0babbdv11--s.gif)
В шейдере для Unity использовались еще и карты нормалей для Matcap, которые запекались с высокополигональной модели. Попробуем сделать в Substance Painter то же самое.
Чтобы использовать все операции над нормалями, нужно подключить соответствующую библиотеку:
![](https://habrastorage.org/webt/6f/zz/z3/6fzzz36x9epa7a34gew0hcq-fro.png)
Теперь подключим карты нормалей. В Substance Painter их две: одна получается путем запекания, а по второй можно рисовать.
![](https://habrastorage.org/webt/hw/7q/86/hw7q86rpk52sdkyygjo32vufv5s.png)
По параметрам можно догадаться, что channel_normal – это карта нормалей, по которой можно рисовать, а texture_normal – запеченная карта нормалей. Отмечу еще, что имя переменной texture_normal вшито в API и назвать ее по своему усмотрению нельзя.
Дальше распаковываем карты в пиксельном шейдере:
![](https://habrastorage.org/webt/yq/wx/sv/yqwxsvn2ohvouv-rwzui9blzp6q.png)
Затем смешиваем карты нормалей и нормали, которые находятся на вертексах модели. Для этого в библиотеке, подключенной выше, есть функция normalBlend().
![](https://habrastorage.org/webt/2y/r0/zs/2yr0zsobknqtlz7ooj_6xd4onsg.png)
Смешиваем сначала две карты нормалей, а потом нормали модели. Хотя на самом деле неважно, в каком порядке их смешивать.
Поворот нормалей в направлении взгляда камеры будет выглядеть так:
![](https://habrastorage.org/webt/yc/bl/mt/ycblmti7x5nhbfhdlx6yyrqyl4o.png)
Дальше можно ничего не менять, всё останется так же. Должно получиться как-то так:
![](https://habrastorage.org/webt/gm/ny/za/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) и теперь уменьшаем оригинальную текстуру в два раза до тех пор, пока она не будет размером в один пиксель. Все версии уменьшенных текстур размещаем справа от оригинальной текстуры в порядке возрастания. Должно получиться вот так:
![](https://habrastorage.org/webt/sg/h-/qw/sgh-qwq1kmwq_gh7hgkg1l8fnxq.jpeg)
Теперь можно приступить к написанию функции, которая будет находить координаты каждой текстуры в пирамиде в зависимости от цвета каждого пикселя на маске.
Проще всего хранить uv-координаты, которые будут рассчитываться для каждой текстуры, в массиве. Размер массива будет определяться как log2(height). Нам нужны оригинальные uv, потому добавим их в аргумент функции. Чтобы определить, какой элемент массива использовать на конкретном пикселе, добавим level в аргумент функции.
![](https://habrastorage.org/webt/hq/ox/um/hqoxumpnar903hkoghv3hgtfzr4.png)
Теперь рассчитаем uv для оригинальной текстуры, то есть обрежем те лишние 128 пикселей по ширине. Для этого достаточно x координату умножить на ?.
![](https://habrastorage.org/webt/ho/or/nh/hoornhxxr9w2yj_bgr8ckt9rdw8.png)
Чтобы использовать остальные текстуры из пирамиды, нужно найти закономерности. Когда мы создавали пирамиду из текстур, то можно было заметить, что каждый раз текстура уменьшается в два раза от предыдущего размера. То есть, во сколько раз уменьшается размер текстуры, можно определить, возводя 2 в степень MIP level.
![](https://habrastorage.org/webt/9i/yk/6k/9iyk6k9udtbo2gyundt8erj8a0q.png)
Получается, если выбрать level, например, 4, то текстура уменьшится в 16 раз. Так как uv-координаты определяются от 0 до 1, то размер нужно нормализовать, то есть 1 разделить на то, во сколько раз уменьшилась текстура, например, 1 разделить на 16.
Используя полученное значение переменной size, можно высчитать координаты для конкретного MIP level.
![](https://habrastorage.org/webt/we/cv/kh/wecvkhragapkffyxyussvrbgkes.png)
Размер uv уменьшается так же, как и размер текстуры. По координате x текстура всегда сдвигается на ?. Сдвиг по координате y можно определить как сумму всех значений переменной size для каждого значения level. То есть если значение level=1, то uv по координате y сдвинутся на 0 пикселей, а если level=2, то сдвиг будет половиной от высоты текстуры – 128 пикселей. Если level=3, то сдвиг получится как 128+64 пикселя и так далее. Сумму всех сдвигов можно получить с помощью цикла.
![](https://habrastorage.org/webt/nj/fl/wi/njflwi83bfg42lecjugixw2xrr8.png)
Теперь каждую итерацию переменная offset будет суммироваться и сдвигать текстуру по оси y на нужное количество пикселей. Пошагово алгоритм выглядит примерно так:
![](https://habrastorage.org/webt/jc/cd/ll/jccdllnuvqh288742jsx21jtbme.gif)
Последним шагом нужно вывести канал, который будет выбирать нужный level на каждом пикселе. Такое мы уже делали, ничего нового.
![](https://habrastorage.org/webt/no/sh/4d/nosh4drvum-rq3fif1mh2qdif_0.png)
![](https://habrastorage.org/webt/vg/-5/w0/vg-5w0rym_gpaygfgzhtkubfxly.png)
Чтобы текстурой выбирать MIP level, достаточно на текстуру умножить длину массива. Теперь можно подключать новые uv-координаты через написанный только что метод.
![](https://habrastorage.org/webt/af/-h/1a/af-h1agpkidgfd7153wgsfgltaa.png)
Не забываем текстуру перевести в тип int, так как это теперь индекс для массива.
Далее нужно в Substance Painter добавить кастомный канал, как это мы делали раньше. Должно получиться так:
![](https://habrastorage.org/webt/qc/4p/h4/qc4ph4jzivfdrtqd5w4dqtn0ojw.gif)
![](https://habrastorage.org/webt/mp/s4/1c/mps41cvu0dlu_kqkkv7qkrtmb5m.gif)
Единственное, чего не хватает для шейдера, это источника света и возможности вращать его нажатием shift. В первую очередь нам для этого понадобится параметр, который будет выдавать угол поворота по нажатию shift, и матрица поворота.
![](https://habrastorage.org/webt/xy/g_/y1/xyg_y1vvg_rhtoxwuepokzrp9x0.png)
![](https://habrastorage.org/webt/ny/17/df/ny17dfcnyibmvgvfx8pbphsxkom.png)
Разместим произвольно источник света и умножим позицию на матрицу вращения.
![](https://habrastorage.org/webt/bo/gi/fp/bogifpq90um2ygho-bgv0zdvy7a.png)
Теперь источник света будет вращаться вокруг оси y по нажатию shift, но пока что это всего лишь вектор, в котором хранится позиция источника света. Есть хороший материал о том, как имплементировать направленный свет в шейдере. Будем ориентироваться на него. Нам осталось определить направление света и освещенность нашей модели.
![](https://habrastorage.org/webt/ok/a4/df/oka4dfn33tv1t2meohooiu9fz2k.png)
Цвет тени и цвет источника света будут задаваться параметрами:
![](https://habrastorage.org/webt/mr/zs/km/mrzskmiirmqmddlhwwg59ab_mpm.png)
Параметры цвета интерполируются по рассчитанной выше освещенности.
![](https://habrastorage.org/webt/c4/ci/8h/c4ci8hm54ul19f-lg92oox2t7li.png)
Получится вот так:
![](https://habrastorage.org/webt/dp/rx/s9/dprxs9gy7_jaw4p3lu_ux7a9xbi.gif)
С помощью этих параметров можно регулировать цвет тени и цвет источника света через интерфейс Substance Painter.
![](https://habrastorage.org/webt/ws/o8/e3/wso8e3nnmdky15efibtajhj0rya.gif)
Когда шейдер готов, нужно импортировать текстуру Matcap и шейдер с настройкой shelf.
![](https://habrastorage.org/webt/hg/w2/wh/hgw2wh4tdzd6i1l4hgcpi8iebpk.png)
Удаляем все неиспользуемые каналы и добавляем user channels:
![](https://habrastorage.org/webt/hl/qs/xe/hlqsxepohrs1j0hbrlkeghgch5g.png)
Пресет для экспорта текстур будет выглядеть как и любой другой, за исключением того, что в нем будут использованы наши кастомные каналы.
![](https://habrastorage.org/webt/zp/xf/7o/zpxf7oje5map2cl0gurvpustk7u.png)
Создадим шаблон всех настроек, чтобы при создании проекта сразу был назначен нужный шейдер и настроены все каналы текстур. Для этого переходим в File/SaveAsTemplate и сохраняем шаблон.
![](https://habrastorage.org/webt/pb/xn/aq/pbxnaqjvwraborjnospf8rglc10.png)
Теперь при создании нового проекта не нужно ничего настраивать – достаточно выбрать нужный темплейт.
![](https://habrastorage.org/webt/hh/xw/by/hhxwbyfzcemnynsqglcabzbgzto.png)
Технический художник может создавать спецэффекты, настраивать сцены и оптимизировать процессы рендеринга. Также я стремился, чтобы модели брони и оружия в игре Stormfall: Saga of Survival были именно такими, какими их задумывали 3D-художники. В результате 3D-модель в Substance Painter выглядит так же, как в игровом движке.
![](https://habrastorage.org/webt/x_/ic/qr/x_icqrlq2s3_osfrps5c-0qttwo.gif)
3D-модель в Substance Painter с кастомным шейдингом.
![](https://habrastorage.org/webt/7e/79/sg/7e79sgwszeyazecwzi18j2fhv4s.gif)
3D-модель в Unity с кастомным шейдингом.
Надеюсь, статья была полезной и вдохновила вас на новые свершения!
![](https://habrastorage.org/webt/tt/hi/z2/tthiz2dpzs2v-tdmzga9nskoeao.png)
Шейдер в юнити
В игре используется Matcap-шейдинг. Помимо обычной диффуз-текстуры, в шейдер еще передается две заранее созданные Matcap-текстуры. Они интерполируются и размываются с помощью двух масок соответственно. В итоге Matcap-текстура умножается на диффузную и на материале можно увидеть фейковые блики и отражения.
![](https://habrastorage.org/webt/wd/mt/fr/wdmtfrjwwe6gach03sv2-u31cya.png)
На примере ниже показано, как реализован Matcap в шейдерграфе. В данном случае две Matcap-текстуры упакованы в одну и разбиты по каналам. То есть металл и неметалл в каналах R и G соответственно.
![](https://habrastorage.org/webt/gt/tn/x1/gttnx1bhczfa3fuqiwmgrqegdvk.jpeg)
Интерполируются два Matcap'а для примера по чекеру.
![](https://habrastorage.org/webt/oj/go/ro/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.
![](https://habrastorage.org/webt/r9/33/bw/r933bw4z7ja0uwi4o8uwoomrthe.gif)
![](https://habrastorage.org/webt/7o/k7/n9/7ok7n9zj0oxq3ouidw_kwt9ezww.jpeg)
В итоге шейдер дает возможность имитировать отражения и блики, добавлять легкие шероховатости и потертости. И это всё без использования Cubemap, без сложных расчетов освещения и других техник, которые значительно снижают производительность мобильных устройств.
![](https://habrastorage.org/webt/ga/yr/ur/gayrur5ty9hr3xdfl56duhlcgqi.gif)
Настройка Substance Painter для создания шейдера
Все доступные шейдеры в Substance Painter написаны на языке GLSL.
Конкретно для написания шейдера под Substance Painter я использую бесплатный VS Code. Для подсветки синтаксиса лучше использовать расширение Shader languages support for VS Code.
![](https://habrastorage.org/webt/fz/xo/iw/fzxoiwxrllvqgwvfammn5uuq7b8.png)
Об API в Substance Painter материалов очень мало, поэтому стандартная документация, которую можно найти в Help/Documentation/Shader API, просто бесценна.
![](https://habrastorage.org/webt/2t/rw/yx/2trwyxswqga9eozdwbk4mmc1ztk.png)
Второе, что будет помогать в написании шейдера, – стандартные шейдеры в Substance Painter. Чтобы их найти, перейдите в .../Allegorithmic/SubstancePainter/resources/shelf/allegorithmic/shaders.
Давайте попробуем написать самый простой unlit-шейдер, который будет показывать Base color. Для начала создадим текстовый файл с расширением .glsl и напишем такой вот несложный шейдер. Возможно, пока что ничего не понятно, я расскажу детальнее о структуре шейдера в Substance Painter дальше.
![](https://habrastorage.org/webt/mk/ck/rv/mkckrvh6j8r7fz7zh72sjfhylgg.png)
Создайте новый проект и перетяните шейдер на ваш shell. В выпадающем списке Import your resources to выберите project ’имя_проекта’.
![](https://habrastorage.org/webt/a0/us/24/a0us24ykiapgfc-ioeuxt7mpckc.png)
Это нужно, чтобы можно было обновлять все изменения.
Теперь перейдите в Window/Views/Shader Settings и в появившемся окне выберите ваш новый шейдер. Можно воспользоваться поиском.
![](https://habrastorage.org/webt/9u/bt/6p/9ubt6padacui5ccb77bgpbywfcg.png)
Если вы увидите, что вся модель белая и по ней можно рисовать Base color, значит, вы всё сделали правильно. Теперь можно сохранить проект и перейти к следующему разделу.
![](https://habrastorage.org/webt/jz/ig/wt/jzigwtwpb73uz6s8paesepkcqao.gif)
Если модель будет розового цвета, то, скорее всего, в шейдере ошибка – уведомление об этом будет в консоли.
Построение шейдера в Substance Painter
Рассмотрим структуру шейдера на примере ранее описанного unlit-шейдера.
![](https://habrastorage.org/webt/mk/ck/rv/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, или матрица вида. Найти такую можно в справке, о которой упоминалось выше.
![](https://habrastorage.org/webt/b5/94/ox/b594oxz6c9doiz0sdm2jbzxt6ow.png)
Итак, это такие же задекларированные названия, как и в случае с Base color. Теперь нам нужно получить нормали 3D-модели.
![](https://habrastorage.org/webt/wg/os/4b/wgos4bygzxftd_sbadht0lqznhy.png)
Ноль как четвертый элемент вектора обязателен.
Перемножение матрицы вида с вектором нормали развернет нормаль к камере.
![](https://habrastorage.org/webt/ty/bt/c0/tybtc0ibyb9ybigfr5x-tse61ne.png)
Не стоит забывать, что при перемножении матриц важен порядок множителей. Если вы измените порядок умножения, результаты будут другими.
Теперь можно из viewNormal создать uv-координаты.
![](https://habrastorage.org/webt/94/ef/jn/94efjnpw9ingufg9ocbgfdxxnwq.png)
Пришло время подключить Matcap-текстуру.
![](https://habrastorage.org/webt/ed/k3/k8/edk3k848a9wdmfa06dunpn8zrni.png)
В данном случае параметр создаст в интерфейсе шейдера поле для текстуры, и если в проекте будет текстура с именем «Matcap_mip», то Substance Painter автоматически подтянет ее.
![](https://habrastorage.org/webt/yl/q5/e0/ylq5e0hbag9jh4bpgj2ro2vot4e.png)
Проверим, что получилось.
![](https://habrastorage.org/webt/re/zb/iy/rezbiydl_fat8ornozz67bwjbs0.png)
Тут текстура Matcap'а раскладывается по новым координатам и на выходе перемножается с Base color. Хочу обратить внимание на то, что текстура Matcap раскладывается через функцию texture(), а Base color – через функцию textureSparse(). Это происходит потому, что текстуры, заданные через интерфейс шейдера, не могут иметь тип SamplerSparse.
Результат должен выглядеть примерно так:
![](https://habrastorage.org/webt/l7/v1/rq/l7v1rqhspgrp92iuffebw2-jmua.gif)
Теперь добавим маску, которая будет смешивать два Matcap'а. Для удобства добавим два Matcap'а в одну текстуру, разбив их по каналам. В итоге две Matcap-текстуры будут в каналах R и G соответственно.
Получится что-то вроде этого:
![](https://habrastorage.org/webt/a-/nr/ih/a-nrih00ttqwnk6ewdv-pbhq9yo.jpeg)
Приступим к добавлению маски в шейдер. Принцип схож с добавлением Base color.
![](https://habrastorage.org/webt/tt/lv/jf/ttlvjfnodtzhu_qp_mpq-oqfpga.png)
Достаточно заменить в параметре значение basecolor на user0.
Теперь достанем значение маски в пиксельном шейдере и смешаем текстуры Matcap.
![](https://habrastorage.org/webt/pa/42/sr/pa42srwn-29afh50di07sepap_w.png)
Здесь в маске используется только канал R, потому что она будет черно-белая. Два канала matcap смешиваются при помощи функции mix() – аналог lerp в Unity.
Давайте обновим шейдер и добавим кастомные каналы в интерфейсе. Для этого нужно перейти в Window/Views/Texture Set Settings, в окне возле заголовка Channels кликнуть на плюс и выбрать из большого списка user0.
![](https://habrastorage.org/webt/0t/v0/mo/0tv0mo6ovcvfztcawlrhbzrhzbs.png)
Канал можно назвать как угодно.
Теперь, рисуя по этому каналу, можно увидеть, как смешиваются две Matcap-текстуры.
![](https://habrastorage.org/webt/ri/jq/nu/rijqnu1-0utez7t0babbdv11--s.gif)
В шейдере для Unity использовались еще и карты нормалей для Matcap, которые запекались с высокополигональной модели. Попробуем сделать в Substance Painter то же самое.
Чтобы использовать все операции над нормалями, нужно подключить соответствующую библиотеку:
![](https://habrastorage.org/webt/6f/zz/z3/6fzzz36x9epa7a34gew0hcq-fro.png)
Теперь подключим карты нормалей. В Substance Painter их две: одна получается путем запекания, а по второй можно рисовать.
![](https://habrastorage.org/webt/hw/7q/86/hw7q86rpk52sdkyygjo32vufv5s.png)
По параметрам можно догадаться, что channel_normal – это карта нормалей, по которой можно рисовать, а texture_normal – запеченная карта нормалей. Отмечу еще, что имя переменной texture_normal вшито в API и назвать ее по своему усмотрению нельзя.
Дальше распаковываем карты в пиксельном шейдере:
![](https://habrastorage.org/webt/yq/wx/sv/yqwxsvn2ohvouv-rwzui9blzp6q.png)
Затем смешиваем карты нормалей и нормали, которые находятся на вертексах модели. Для этого в библиотеке, подключенной выше, есть функция normalBlend().
![](https://habrastorage.org/webt/2y/r0/zs/2yr0zsobknqtlz7ooj_6xd4onsg.png)
Смешиваем сначала две карты нормалей, а потом нормали модели. Хотя на самом деле неважно, в каком порядке их смешивать.
Поворот нормалей в направлении взгляда камеры будет выглядеть так:
![](https://habrastorage.org/webt/yc/bl/mt/ycblmti7x5nhbfhdlx6yyrqyl4o.png)
Дальше можно ничего не менять, всё останется так же. Должно получиться как-то так:
![](https://habrastorage.org/webt/gm/ny/za/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) и теперь уменьшаем оригинальную текстуру в два раза до тех пор, пока она не будет размером в один пиксель. Все версии уменьшенных текстур размещаем справа от оригинальной текстуры в порядке возрастания. Должно получиться вот так:
![](https://habrastorage.org/webt/sg/h-/qw/sgh-qwq1kmwq_gh7hgkg1l8fnxq.jpeg)
Теперь можно приступить к написанию функции, которая будет находить координаты каждой текстуры в пирамиде в зависимости от цвета каждого пикселя на маске.
Проще всего хранить uv-координаты, которые будут рассчитываться для каждой текстуры, в массиве. Размер массива будет определяться как log2(height). Нам нужны оригинальные uv, потому добавим их в аргумент функции. Чтобы определить, какой элемент массива использовать на конкретном пикселе, добавим level в аргумент функции.
![](https://habrastorage.org/webt/hq/ox/um/hqoxumpnar903hkoghv3hgtfzr4.png)
Теперь рассчитаем uv для оригинальной текстуры, то есть обрежем те лишние 128 пикселей по ширине. Для этого достаточно x координату умножить на ?.
![](https://habrastorage.org/webt/ho/or/nh/hoornhxxr9w2yj_bgr8ckt9rdw8.png)
Чтобы использовать остальные текстуры из пирамиды, нужно найти закономерности. Когда мы создавали пирамиду из текстур, то можно было заметить, что каждый раз текстура уменьшается в два раза от предыдущего размера. То есть, во сколько раз уменьшается размер текстуры, можно определить, возводя 2 в степень MIP level.
![](https://habrastorage.org/webt/9i/yk/6k/9iyk6k9udtbo2gyundt8erj8a0q.png)
Получается, если выбрать level, например, 4, то текстура уменьшится в 16 раз. Так как uv-координаты определяются от 0 до 1, то размер нужно нормализовать, то есть 1 разделить на то, во сколько раз уменьшилась текстура, например, 1 разделить на 16.
Используя полученное значение переменной size, можно высчитать координаты для конкретного MIP level.
![](https://habrastorage.org/webt/we/cv/kh/wecvkhragapkffyxyussvrbgkes.png)
Размер uv уменьшается так же, как и размер текстуры. По координате x текстура всегда сдвигается на ?. Сдвиг по координате y можно определить как сумму всех значений переменной size для каждого значения level. То есть если значение level=1, то uv по координате y сдвинутся на 0 пикселей, а если level=2, то сдвиг будет половиной от высоты текстуры – 128 пикселей. Если level=3, то сдвиг получится как 128+64 пикселя и так далее. Сумму всех сдвигов можно получить с помощью цикла.
![](https://habrastorage.org/webt/nj/fl/wi/njflwi83bfg42lecjugixw2xrr8.png)
Теперь каждую итерацию переменная offset будет суммироваться и сдвигать текстуру по оси y на нужное количество пикселей. Пошагово алгоритм выглядит примерно так:
![](https://habrastorage.org/webt/jc/cd/ll/jccdllnuvqh288742jsx21jtbme.gif)
Последним шагом нужно вывести канал, который будет выбирать нужный level на каждом пикселе. Такое мы уже делали, ничего нового.
![](https://habrastorage.org/webt/no/sh/4d/nosh4drvum-rq3fif1mh2qdif_0.png)
![](https://habrastorage.org/webt/vg/-5/w0/vg-5w0rym_gpaygfgzhtkubfxly.png)
Чтобы текстурой выбирать MIP level, достаточно на текстуру умножить длину массива. Теперь можно подключать новые uv-координаты через написанный только что метод.
![](https://habrastorage.org/webt/af/-h/1a/af-h1agpkidgfd7153wgsfgltaa.png)
Не забываем текстуру перевести в тип int, так как это теперь индекс для массива.
Далее нужно в Substance Painter добавить кастомный канал, как это мы делали раньше. Должно получиться так:
![](https://habrastorage.org/webt/qc/4p/h4/qc4ph4jzivfdrtqd5w4dqtn0ojw.gif)
![](https://habrastorage.org/webt/mp/s4/1c/mps41cvu0dlu_kqkkv7qkrtmb5m.gif)
Единственное, чего не хватает для шейдера, это источника света и возможности вращать его нажатием shift. В первую очередь нам для этого понадобится параметр, который будет выдавать угол поворота по нажатию shift, и матрица поворота.
![](https://habrastorage.org/webt/xy/g_/y1/xyg_y1vvg_rhtoxwuepokzrp9x0.png)
![](https://habrastorage.org/webt/ny/17/df/ny17dfcnyibmvgvfx8pbphsxkom.png)
Разместим произвольно источник света и умножим позицию на матрицу вращения.
![](https://habrastorage.org/webt/bo/gi/fp/bogifpq90um2ygho-bgv0zdvy7a.png)
Теперь источник света будет вращаться вокруг оси y по нажатию shift, но пока что это всего лишь вектор, в котором хранится позиция источника света. Есть хороший материал о том, как имплементировать направленный свет в шейдере. Будем ориентироваться на него. Нам осталось определить направление света и освещенность нашей модели.
![](https://habrastorage.org/webt/ok/a4/df/oka4dfn33tv1t2meohooiu9fz2k.png)
Цвет тени и цвет источника света будут задаваться параметрами:
![](https://habrastorage.org/webt/mr/zs/km/mrzskmiirmqmddlhwwg59ab_mpm.png)
Параметры цвета интерполируются по рассчитанной выше освещенности.
![](https://habrastorage.org/webt/c4/ci/8h/c4ci8hm54ul19f-lg92oox2t7li.png)
Получится вот так:
![](https://habrastorage.org/webt/dp/rx/s9/dprxs9gy7_jaw4p3lu_ux7a9xbi.gif)
С помощью этих параметров можно регулировать цвет тени и цвет источника света через интерфейс Substance Painter.
![](https://habrastorage.org/webt/ws/o8/e3/wso8e3nnmdky15efibtajhj0rya.gif)
Создание и настройка пресета
Когда шейдер готов, нужно импортировать текстуру Matcap и шейдер с настройкой shelf.
![](https://habrastorage.org/webt/hg/w2/wh/hgw2wh4tdzd6i1l4hgcpi8iebpk.png)
Удаляем все неиспользуемые каналы и добавляем user channels:
![](https://habrastorage.org/webt/hl/qs/xe/hlqsxepohrs1j0hbrlkeghgch5g.png)
Пресет для экспорта текстур будет выглядеть как и любой другой, за исключением того, что в нем будут использованы наши кастомные каналы.
![](https://habrastorage.org/webt/zp/xf/7o/zpxf7oje5map2cl0gurvpustk7u.png)
Создадим шаблон всех настроек, чтобы при создании проекта сразу был назначен нужный шейдер и настроены все каналы текстур. Для этого переходим в File/SaveAsTemplate и сохраняем шаблон.
![](https://habrastorage.org/webt/pb/xn/aq/pbxnaqjvwraborjnospf8rglc10.png)
Теперь при создании нового проекта не нужно ничего настраивать – достаточно выбрать нужный темплейт.
![](https://habrastorage.org/webt/hh/xw/by/hhxwbyfzcemnynsqglcabzbgzto.png)
Что получили
Технический художник может создавать спецэффекты, настраивать сцены и оптимизировать процессы рендеринга. Также я стремился, чтобы модели брони и оружия в игре Stormfall: Saga of Survival были именно такими, какими их задумывали 3D-художники. В результате 3D-модель в Substance Painter выглядит так же, как в игровом движке.
![](https://habrastorage.org/webt/x_/ic/qr/x_icqrlq2s3_osfrps5c-0qttwo.gif)
3D-модель в Substance Painter с кастомным шейдингом.
![](https://habrastorage.org/webt/7e/79/sg/7e79sgwszeyazecwzi18j2fhv4s.gif)
3D-модель в Unity с кастомным шейдингом.
Надеюсь, статья была полезной и вдохновила вас на новые свершения!
HexGrimm
Заметил что на шариках в гифках отражение не правильно смещается. Получается как буд-то шарик полупрозрачный как ёлочная игрушка. Должно же быть не так?
Plarium Автор
Хорошее замечание. Для записи гифок использовалась сфера без полюсов (куб с несколькими сабдивами), на такой модели маткап будет выглядеть как вы подметили, со смещением. Должно ли быть так? Если использовать модель сферы аналогичную той, которая на гифках, то да, должно.
HexGrimm
Я имел в виду, не должно ли смещаться в противоположную сторону? Мне кажется так чувство объёма меша будет лучше.
Plarium Автор
Статья в большей мере направлена на демонстрацию возможностей shader api в Substance Painter. Что насчет отражений, то мы использовали популярный вариант реализации маткапа, не погружаясь в ее нюансы. Стоит ли подумать над тем как улучшить ощущение объема на модели? Да, можно придумать что-то более интересное, чем просто маткап, но это уже немного другая тема для обсуждения.