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

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



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


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



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



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



В итоге получается некая аналогия с металлом и неметаллом как в 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.





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



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


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



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



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

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



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



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

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



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



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

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


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



Метод 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, или матрица вида. Найти такую можно в справке, о которой упоминалось выше.



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


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

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



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

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



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



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



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



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

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



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

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



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



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

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



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

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



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

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



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

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



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



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

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



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



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

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



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



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

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



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

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



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



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



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

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



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



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



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





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



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





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





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



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



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



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



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



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



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


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



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



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



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



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



Что получили


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


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


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

Надеюсь, статья была полезной и вдохновила вас на новые свершения!
Plarium
Разработчик мобильных и браузерных игр

Комментарии 6

    0
    Заметил что на шариках в гифках отражение не правильно смещается. Получается как буд-то шарик полупрозрачный как ёлочная игрушка. Должно же быть не так?
      0
      Хорошее замечание. Для записи гифок использовалась сфера без полюсов (куб с несколькими сабдивами), на такой модели маткап будет выглядеть как вы подметили, со смещением. Должно ли быть так? Если использовать модель сферы аналогичную той, которая на гифках, то да, должно.
        0
        Я имел в виду, не должно ли смещаться в противоположную сторону? Мне кажется так чувство объёма меша будет лучше.
          0
          Статья в большей мере направлена на демонстрацию возможностей shader api в Substance Painter. Что насчет отражений, то мы использовали популярный вариант реализации маткапа, не погружаясь в ее нюансы. Стоит ли подумать над тем как улучшить ощущение объема на модели? Да, можно придумать что-то более интересное, чем просто маткап, но это уже немного другая тема для обсуждения.
      0
      То есть через шейдерграф вам так и не удалось получить приемлемые шейдеры для мобильных платформ? Насколько он вообще юзабелен?)
        0
        В игре Strormfall: Saga of Survival большинство шейдеров написаны вручную и хорошо оптимизированы. Скриншоты в статье из шейдерграфа демонстрируют лишь основные трюки, аналогичные трюкам в шейдере из проекта. Что насчет URP с шейдерами из шейдерграфа на мобильных платформах, то между ними нет особых препятствий. Шейдера из нодовых редакторов получаются слегка не оптимизированные, однако насколько это критично, больше зависит от сложности самого шейдера.

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

      Самое читаемое