Shader Graph в Unity 6: что реально стоит делать визуально, а что писать в HLSL
Shader Graph я долго не любил. Пять лет писал шейдеры руками в HLSL, и каждый раз, когда видел граф из 200 нод, думал: «понятный код был бы в три раза короче». Потом доверился, переписал на Shader Graph треть шейдеров одного проекта, и пересмотрел отношение. Не на всё, но во многих случаях это правильный инструмент. Расскажу, где он окупается, где нет, и какие приёмы окажутся полезны.
Опираюсь на Unity 6 (6000.x), Shader Graph 17.x, URP. В HDRP всё похоже, но Master-нода другая.
Что такое Shader Graph и зачем он
Shader Graph — визуальный редактор шейдеров. Соединяешь ноды (Sample Texture, Add, Multiply, Lerp), на выходе получаешь шейдер, совместимый с URP/HDRP. Compile target — HLSL под капотом, Unity сама собирает код.
Главные плюсы:
- Художники, не пишущие код, могут тыкать материал и сразу видеть результат.
- Препревью каждой ноды — мгновенная визуализация.
- SRP Batcher-friendly: граф автоматически использует CBUFFER и совместим с батчингом.
- Кросс-пайплайн: те же ноды работают в URP и HDRP с минимальными правками мастер-нод.
- Subgraph'ы для переиспользования. Один раз собрал triplanar mapping — кладёшь в subgraph, используешь везде.
Главные минусы:
- Сложные графы трудно читать. 200 нод — это лабиринт, в котором без названий и комментариев не разберёшься.
- Performance не всегда оптимальный. Shader Graph генерирует «честный» код, без хитрых оптимизаций. Опытный человек на HLSL напишет на 10–20% быстрее.
- Diff'ы в git — бинарные. Merge невозможен, ревью graf'а — только через скриншоты.
- Не все шейдерные техники доступны через ноды (multi-pass effects, geometry shaders, tessellation — частично или никак).
Когда Shader Graph оправдан
По моему опыту — когда у тебя:
- Стилизованные эффекты, где итерации художника важнее экономии 0.05 мс на фрейме. Toon-шейдеры, dissolve, силуэты, hologram-эффекты, водопады.
- Несколько вариаций похожих шейдеров, где Subgraph'ы реально экономят. Тип «у меня 10 разных материалов с одной базовой логикой плюс уникальная фича каждого».
- Художник в команде хочет править шейдеры сам.
- Прототип/гейм-джем, где нужна скорость.
Когда я остаюсь на HLSL:
- Performance-критичные шейдеры на мобиле. Каждый ALU имеет значение, и иметь явный код важно.
- Сложные lighting-функции (custom BRDF, anisotropic shading). В Shader Graph есть Custom Function ноды, но они становятся костылями.
- Multi-pass эффекты, где нужно несколько проходов с разными состояниями.
- Compute shaders, surface shaders, geometry shaders.
Базовый стилизованный шейдер: dissolve
Один из самых частых эффектов — диссоль (растворение объекта по noise-маске). Хорошо ложится на граф.
Шаги:
- Master-нода: Universal Lit или Universal Unlit.
- Sample Texture 2D — для базовой текстуры.
- Sample Texture 2D — для noise (Simplex Noise или Voronoi из коробки тоже работает).
- Subtract: noise.r минус параметр _DissolveAmount (Float, expose в Inspector).
- Step или smoothstep — порог.
- В Alpha Clipping мастер-ноды подаём результат step'а инверсно (1 - step).
- Доп: Edge color через cremrer цветной полоски на границе исчезновения.
В коде это 30–40 строк HLSL, с фрагмент-функцией. В графе — 15 нод. Художник может править _DissolveAmount, цвет края и noise-текстуру в инспекторе. Победа.
Custom Function ноды: ваш мостик в HLSL
В графе есть нода Custom Function. Можешь написать кусок HLSL и подключить как ноду.
// SmoothMin.hlsl
void SmoothMin_float(float a, float b, float k, out float result)
{
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
result = lerp(b, a, h) - k * h * (1.0 - h);
}
В графе подключаешь как Custom Function, указываешь файл и имя функции, поля Input/Output. Это спасает, когда ноды для нужной операции нет.
Совет: не пихай в Custom Function огромные блоки. Если у тебя fragment-шейдер целиком в HLSL внутри одной Custom Function — это знак, что Shader Graph не подходит, пиши классический шейдер.
Subgraph'ы: убийцы дублирования
Самая полезная фича для больших проектов. Subgraph — это мини-граф, который можно вставить как ноду в основной.
Что я держу в Subgraph'ах:
- Triplanar mapping (универсальная штука для скал, террейна).
- Distance fade (плавное скрытие объекта при приближении камеры).
- Vertex displacement по noise (трава, листья).
- UV scrolling с настраиваемой скоростью.
- Fresnel + цвет — для рим-лайта.
Каждый Subgraph — один файл, переиспользуется во всех материалах. Меняешь логику в Subgraph — обновляются все шейдеры, его использующие.
Производительность: что смотреть
Shader Graph не магически дороже, но и не дешевле руки. Что я мониторю:
Number of instructions
В Frame Debugger или через Show Generated Code (правый клик на мастер-ноде) видно итоговый HLSL. Можешь прикинуть количество ALU и текстурных сэмплов. Для мобильного fragment-шейдера норма — до 50–60 ALU и 4–5 текстур.
Complex Lit vs Lit
В URP в Shader Graph есть Lit и Complex Lit. Complex поддерживает clear coat, anisotropic — больше фич, но дороже. Для мобилки бери Lit.
Динамические сэмплы
Если в графе ты сэмплишь текстуру по координатам, рассчитанным во фрагмент-шейдере (с зависимостями), GPU не может предсказать LOD, и качество фильтрации страдает. Где можно — переноси сэмплинг в вершинный шейдер.
Boolean keywords
В Blackboard'е графа можно объявить Boolean или Enum Keyword. Они компилируются в варианты шейдера через #pragma multi_compile. Полезно для опциональных фич, но количество keywords умножается комбинаторно. Если у тебя 5 keywords, это 32 варианта шейдера. Размер билда и compile time страдают.
SRP Batcher friendliness
SRP Batcher объединяет draw call'ы одинаковых шейдеров с разными материалами. Условие — все uniform'ы материала в одном CBUFFER.
Shader Graph сам генерит правильный CBUFFER. Custom HLSL шейдеры могут это сломать, если не следить. Проверка в инспекторе шейдера — поле SRP Batcher: compatible. Должно быть «yes».
Прозрачность и сортировка
Самый частый затык — прозрачные материалы. В мастер-ноде ставишь Surface: Transparent, и тут начинается:
- По умолчанию depth write выключен — объект пишется в depth, но не сортируется. Получаешь визуальные баги «один прозрачный объект перед/за другим случайно».
- В URP ты часто хочешь прозрачные объекты с включённым depth write для эффектов вроде glass-orb с преломлением.
- Внутри одного меша несколько прозрачных submesh'ей рендерятся в порядке индексов, а не глубины.
Решения зависят от случая, но я почти всегда:
- Стараюсь использовать Alpha Clipping (cutout) вместо Transparent, где можно. Cutout сортируется правильно и быстрее.
- Для дыма/частиц — soft particles, специальный transparent setup.
- Для стёкол — отдельный пасс с depth-prepass'ом.
Vertex displacement
Shader Graph поддерживает выход в Position (мастер-нода) — это вершинный шейдер. Базовое смещение вершин по noise или sin'у — несколько нод.
Что важно:
- Если двигаешь вершины, не забудь нормали пересчитать. Иначе lighting будет неправильным. Это или вычисление в Subgraph'е (производные), или передача через UV2 готовых нормалей.
- Для травы/листьев — World Position должен влиять на смещение, не Local. Иначе все травинки качаются в одной фазе.
- На мобилке движение вершин дешевле, чем фрагмент-эффекты, но не бесплатно. На дальних LOD'ах отключай.
Workflow в команде
Несколько практик, которые сэкономили мне время:
- Naming convention. Каждая нода с осмысленным именем (правый клик → rename). Не «Multiply (3)», а «Mask × Power».
- Группы. Граф разбивай на логические группы через GroupNode. Каждая группа — одна функция (например, «Diss UV», «Edge Color», «Fresnel»).
- Comments. Sticky Notes — добавляй пояснения, особенно для нестандартных решений.
- Versioning. Сохраняй стабильные версии материала. Когда художник экспериментирует, пусть это делает в дубликате.
- Code review. Перед merge'ом graf'а — скриншот в PR. Без визуала ревьюверу не понять, что произошло.
Hybrid подход
На больших проектах я работаю гибридно. Базовый шейдер — на Shader Graph (визуально, художник правит). Сложная математика (например, custom lighting) — в HLSL, подключается через Custom Function.
Так получается: художник управляет визуальными параметрами, программист — производительностью и алгоритмами. Никто не блокирует никого.
Что не делать
- Не пиши скрипты, которые в рантайме модифицируют граф. Это оверкомпликация. Если нужны параметры — expose их в Blackboard и меняй SetFloat/SetVector на материале.
- Не используй Shader Graph для compute shader'ов. Не его задача.
- Не меняй мастер-ноду «по ходу проекта». Если начал на Lit, и потом перешёл на Unlit — все настройки слетят. Решай в начале.
- Не делай 200 keywords. Помни про комбинаторный взрыв вариантов.
Что почитать и посмотреть
Cyanilux на YouTube — лучший источник по Shader Graph: и базовые туториалы, и сложные эффекты. Daniel Ilett — реалистичные техники (rim light, tessellation, ray marching) в графе и HLSL. Ben Cloward — старше, но фундаментальные основы шейдеров.
Из репозиториев — официальный Shader Graph Examples от Unity и URP Code Templates на GitHub. Там примеры кастомных Renderer Features в связке с графами.
Главное правило, которое сэкономит время: сначала пробуй на Shader Graph. Если упёрся в производительность или фичи — переходи на HLSL для конкретного шейдера. Не переделывай весь проект разом.