Поддержка TextMesh Pro в Soft Mask

TextMesh Pro — очень популярный ассет для рендеринга SDF-шрифтов. Не так давно он стал доступен бесплатно, что для меня послужило поводом наконец-то сделать штатную интеграцию этого пакета с моим Soft Mask.

Как я упоминал в прошлой статье, для этого мне потребовалось немного доработать TextMesh Pro. С тех пор Stephan Bouchard включил мои изменения в очередную версию, а я — доделал интеграцию и отправил пакет в Asset Store. Так что сейчас самое время немного передохнуть и рассказать о том, что я сделал.

Комбинаторные проблемы

Когда я впервые познакомился с подходом Unity к шейдерам, я был, мягко говоря, удивлен. Нет, если бы я познакомился с ним, скажем, в 2009 году, то все бы ничего. Но я впервые всерьез взялся за Unity в 2016, и по меркам 2016 подход а-ля D3DXEffect мне показался… немного устаревшим. Я ожидал какие-нибудь визуальные редакторы в духе Unreal Engine или, там, Blender Cycles. Или какую-нибудь хитрую компиляцию C# под GPU. А вместо этого старый добрый монолитный “эффект”.

Использование монолитного эффекта означает, что мы рисуем какой-то элемент каким-то одним шейдером. Это может быть Standard Shader, или какой-нибудь шейдер для частиц или, скажем, шейдер для UI. А если нужно что-то нестандартное, нужно делать свой шейдер. Целиком.

Это приводит к огромному количеству копипасты. В стандартной библиотеке ее полно, на Asset Store ее полно, а уж в каждом проекте на Unity — ну просто навалом. Смотрите сами: в Unity есть стандартный шейдер для UI (на самом деле — два, но они на 99% одинаковые), в Soft Mask есть свой вариант этого шейдера (на самом деле — двух, ну чтобы все везде работало), в TextMesh Pro также есть несколько вариантов этого шейдера.

Нет, конечно, Unity молодцы и сократили копипасту насколько могли, вынесли всякие #include’ы, придумали surface shaders. Но сам .shader все равно должен быть отдельным. Значит, в проекте мы уже получаем кучу похожих шейдеров. Это не проблема, пока все эти копии поддерживаются разными разработчиками. Но что, если разработчику захочется совместить Soft Mask и TextMesh Pro? Или любые другие два ассета? А если, о боже, три? Все правильно, нужно просто руками создать нужные комбинации шейдеров.

Для пользователей своего пакета я хотел лучшей жизни. На самом деле, сделать интеграцию в некотором виде можно было всегда (ну, почти, см. далее). Я специально предусмотрел шейдерное API, вынес все в #include, чтобы программист, при желании, мог прикрутить Soft Mask к любому UI-шейдеру. Но это процесс довольно муторный, а для некоторых — слишком сложный (ведь не только программисты хотят запилить игру своей мечты). Мне хотелось, чтобы все работало из коробки. А это значит, что мне нужно просто скопировать шейдеры TextMesh Pro и адаптировать их для работы с Soft Mask. И потом обновлять их после каждого обновления TextMesh Pro…

Ну уж нет. На такое я не был согласен. Даже для любимых пользователей. Я уж не говорю о том, что распространять часть чужого ассета, пускай и патченную — дело весьма спорное с точки зрения EULA.

Посему пришлось искать альтернативное решение.

Патчинг

Решение, конечно, быстро нашлось. Пойдем по стопам любителей вносить кастомные правки в open-source проекты. Под себя, так сказать. И перевносить их каждый раз при обновлении. Я просто сделаю, чтобы после установки интеграционного пакета из шейдеров TextMesh Pro автоматически создавались патченные версии. Тогда мне нужно будет распространять только патчи, что нарушением EULA посчитать уже сложно. И, дополнительная плюшка — патч может покрывать более одной версии TextMesh Pro, если шейдеры не сильно менялись.

Задумка мне очень нравилась, она такая… технологичная! Есть в этом что-то крутое! Оставалось только найти какой-нибудь способ накатить патчи.

Желание поскорее решить задачу перевесило желание “крутости”, так что я не стал списы писать алгоритм самостоятельно. Вместо этого нашел библиотечку Diff Match and Patch от Google. Под Apache-лицензией, что для Asset Store, по моим представлением, как раз приемлемо. Для интеграции этого добра в Unity потребовалось докинуть еще MonoHttp из RestSharp. Тоже под Apache, к счастью.

Для создания патчей я сделал свои копии шейдеров TextMesh Pro и внес в них нужные для Soft Mask правки. Затем натравил на них простенький скриптик, который при помощи гугловской библиотеки создает патчи. Все эти патчи сохраняются в ScriptableObject, который уже распространяется в составе интеграционного пакета. Чтобы избежать лишней работы, я добавил аннотацию [Serializable] к одному из классов Diff Match and Patch, благо Apache-лицензия это допускает.

После этого другой скрипт, уже входящий в состав интеграционного пакета, генерирует патченные шейдеры. Ну а для того, чтобы шейдеры TextMesh Pro подменялись во время выполнения патченными, в Soft Mask был добавлен специальный механизм, позволяющий встроиться в алгоритм замены материалов. Вот как этот механизм используется в интеграционном пакете:

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

И снова патчинг

Я уже не раз упоминал о том, что мне пришлось немного поправить TextMesh Pro, чтобы все заработало. В чем, собственно, была проблема?

Дело в том, что TextMeshProUGUI, хоть и наследуется от Graphic, при этом напрочь игнорирует IMaterialModifier. Базовый Graphic имеет такую реализацию свойства materialForRendering:

Как видим, метод последовательно применяет все IMaterialModifier’ы, лежащие на объекте. А реализация этого метода в TextMesh Pro выглядит вот так:

Где GetModifiedMaterial() — это метод собственно IMaterialModifier’а. Да, тут все немного запутано. Дело в том, что MaskableGraphic сам является IMaterialModifier’ом. Это позволяет ему сделать подмену обычного материала материалом с правильно выставленными настройками stencil’а, что используется стандартной маской. Собственно, TextMeshProUGUI, как и все стандартные графические элементы (кроме каретки и подсветки текста!) наследуется именно от MaskableGraphic, а не от Graphic. И нет, GetModifiedMaterial() не делает и не может делать применение IMaterialModifier’ов. Ну… зациклится ведь.

Собственно, TextMeshProUGUI никакие модификаторы к материалам не применяет. И Soft Mask просто не работает, даже если используемые шейдеры и поддерживают маскирование. Именно поэтому “обычные пользователи” и не могли самостоятельно добавить поддержку TextMesh Pro. Был придуман один workaround с наследованием, который я успел посоветовать нескольким пользователям, но он был крайне непрактичен, т.к. требовал замены стандартного TextMeshProUGUI каким-нибудь нестандартным TextMeshPtoUGUIFixed. А в Unity нет штатного способа перенести свойства объекта одного класса на объект другого класса. А свойств переносить нужно много. Так что в реальных проектах такой подход слабо применим.

Было еще одно расхождение с работой базового Graphic: не вызывался dirty material callback. Но это довольно тривиальная правка.

Может сложиться впечатление, будто я критикую или обвиняю автора TextMesh Pro в том, что его код что-то там делает не так. Вовсе нет. Я прекрасно понимаю, что TextMesh Pro появился еще до Unity UI и что сам Unity UI тоже мог изменяться в процессе. Так что какие-то вещи могли пройти незамеченными. Так бывает.

Последние штрихи

Было еще немного возни с сэмплами. Я долго маялся, пытаясь понять, какие, собственно, сэмплы сделать. Хотел было сделать что-нибудь приближенное к реальным кейсам, договориться с каким-нибудь художником с Asset Store и включить немного его UI-арта в свои сэмплы. Первый понравившийся художник отказал, после чего я еще поразмышлял… и решил, что к черту это все. В интеграционном пакете вообще нет большой нужды в сэмплах — люди, которые его качают, и так прекрасно понимают, зачем им TextMesh Pro и Soft Mask. Ну, по крайней мере, мне так кажется.

Поэтому я решил просто взять пару сэмплов из Soft Mask и заменить в них обычный текст на TextMesh Pro. Тут же всплыла небольшая сложность: дело в том, что существует 2 версии TextMesh Pro: бинарная и в виде исходников. Бинарная — это “обычная” версия, которая сейчас бесплатно доступна на Asset Store от имени Unity Technologies. А версия в исходном коде — это то, что было до того, как Unity выкупили этот продукт. И эти две версии немного не совместимы, у них разные GUID’ники скриптов. Поэтому проекты, которые использовали “старую” платную версию, вынуждены продолжать использовать ее и брать обновления с закрытого раздела форума TextMesh Pro.

Как вы уже догадались, это затрагивает не только все конечные проекты, использующие TextMesh Pro, но и все Asset’ы, которые желают интегрироваться с TextMesh Pro. То есть, любая ссылка на скрипт TextMesh Pro, скажем TextMeshProUGUI или TMP_FontAsset, будет работать либо в платной версии, либо в бесплатной. Мне, по большей части, удалось избежать этого, т.к. мой основной код вообще не ссылается на TextMesh Pro. А вот сэмплы как раз используют означенные выше скрипты.

Я довольно долго размышлял над этой ситуацией, пожалуй, больше, чем следовало. И в итоге остановился на весьма незатейливом варианте: включить в интеграционный пакет 2 копии сэмплов: одну для платного TextMesh Pro, другую — для бесплатного. А чтобы не делать 2 версии руками, написал простенькой скрипт, который делает одну версию из другой, тупо подменяя ссылки в сценах и префабах 🙂

Результат

Вся эта эпопея с TextMesh Pro заняла несколько месяцев — просто уйму времени по меркам такого маленького продукта, как Soft Mask! Конечно, большая часть времени ушла на различные ожидания. Но, в целом, я пока что доволен результатом:

На данный момент интеграционный пакет находится на рассмотрении в Asset Store. Похоже, что сей процесс займет много времени, т.к. в автоматическом письме было упоминание о том, что они перегружены и сейчас все занимает больше времени, чем обычно. А оно и раньше занимало много времени!

Лично мне очень любопытно увидеть реакцию пользователей на “официальную” поддержку TextMesh Pro. С одной стороны, это был, пожалуй, самый частый запрос. С другой стороны, пользователей не так уж и много и, возможно, все, кому это было нужно, уже решили вопрос самостоятельно. Что ж, увидим.

Leave a Reply

Your email address will not be published. Required fields are marked *