Как разработать собственные шейдеры для Unity URP

Как разработать собственные шейдеры для Unity URP

Разработка собственных шейдеров в Unity URP позволяет управлять тем, как именно движок отображает поверхность, свет, тени, прозрачность и постобработку.

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

В Universal Render Pipeline собственные шейдеры особенно полезны для мобильных игр, виртуальной реальности, интерактивных презентаций, архитектурных визуализаций и технических симуляторов. Один и тот же эффект можно реализовать несколькими способами: через Shader Graph, ручной HLSL-код или комбинацию обоих подходов.

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

Разберём архитектуру URP, устройство вершинных и фрагментных программ, структуру ShaderLab-файла, работу со светом, тенями, нормалями, прозрачностью и вариантами шейдеров.

Отдельное внимание уделим оптимизации: по данным практических проектов, сокращение лишних вычислений во фрагментном шейдере иногда даёт прирост производительности на десятки процентов, тогда как одна дорогостоящая текстурная выборка в мобильном сценарии может оказаться заметнее, чем несколько простых арифметических операций.

Что это шейдер в Unity URP

Шейдер программа, выполняемая графическим процессором. В отличие от обычного кода игры, который чаще всего работает на центральном процессоре, шейдер обрабатывает большое количество вершин и пикселей параллельно.

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

В классическом растровом конвейере сначала обрабатываются вершины модели.

Вершинная программа получает координаты, нормали, цвета и UV-координаты, преобразует их из локального пространства в экранное и передаёт промежуточные данные дальше.

После растеризации фрагментная программа вычисляет итоговый цвет каждого видимого фрагмента с учётом текстур, света, теней, нормалей и параметров материала.

В Unity URP шейдер взаимодействует не только с моделью и материалом, но и с системой рендеринга.

Universal Render Pipeline предоставляет функции для работы с пространствами координат, освещением, теневыми картами, дополнительными источниками света, fog-эффектом и различными типами поверхностей. Благодаря этому разработчику не приходится вручную реализовывать весь графический конвейер.

Важно различать шейдер и материал. Шейдер содержит код и описание доступных параметров, а материал хранит конкретные значения этих параметров: цвет, текстуру, интенсивность свечения, коэффициент шероховатости или масштаб UV.

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

Когда выбрать Shader Graph, а когда HLSL

Shader Graph - визуальный редактор, в котором логика шейдера собирается из узлов.

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

Главное преимущество Shader Graph заключается в скорости экспериментов. Значение можно передать из одного узла в другой, изменить тип операции и сразу увидеть результат в окне предпросмотра.

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

Ручной HLSL-код предпочтителен, когда требуется точный контроль над структурой данных, количеством проходов, условной компиляцией и интеграцией с нестандартным рендер-процессом.

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

На практике разумно использовать оба подхода. Shader Graph подходит для большинства художественных эффектов, а HLSL - для низкоуровневых функций, вычислений и повторно используемых библиотек. Unity позволяет подключать пользовательские HLSL-фрагменты к графу, поэтому визуальное описание материала не обязательно исключает программный код.

Задача Подход Причина выбора
Простая эмиссия и цветовая анимация Shader Graph Быстрая настройка и удобный просмотр результата
Кастомное освещение HLSL или Shader Graph с пользовательскими функциями Необходим контроль над математикой света
Мобильный материал с минимальным числом инструкций HLSL Проще контролировать производительность
Прототип технического эффекта Shader Graph Быстрое изменение параметров без переписывания кода

Подготовка проекта и проверка окружения

Перед написанием шейдера необходимо определить версию Unity и URP. Синтаксис некоторых функций, имена include-файлов и доступные узлы Shader Graph меняются между версиями. Код, который корректно компилируется в одной версии, может потребовать небольших изменений в другой.

Создайте отдельную тестовую сцену. В ней желательно иметь серую сферу, плоскость, куб, объект со сложной нормалью и несколько источников света. Такой набор помогает быстро заметить ошибки: сферическая модель показывает работу нормалей, плоскость демонстрирует UV и тени, а куб позволяет увидеть проблемы с пространствами координат.

Для анализа результата используйте окно Frame Debugger, профайлер Unity и инструменты профилирования графического процессора. На компьютере можно дополнительно применять RenderDoc, если целевая конфигурация это допускает.

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

Параметры материала следует заранее разделить на художественные и технические. К художественным относятся цвет, сила свечения, масштаб текстуры и скорость анимации.

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

Структура простого шейдера URP

Файл шейдера Unity обычно состоит из блока Shader, одного или нескольких блоков SubShader и одного или нескольких Pass. Внутри находятся свойства материала, настройки рендеринга, ссылки на HLSL-файлы и функции вершинной и фрагментной обработки.

Ниже приведён упрощённый пример шейдера, который окрашивает объект в заданный цвет. Он предназначен для понимания общей структуры, а не для замены полноценного Lit-материала.

Shader "HiTech/URP/SimpleColor"
{
 Properties
 {
 _BaseColor ("Base Color", Color) = (0.2, 0.6, 1.0, 1.0)
 }

 SubShader
 {
 Tags
 {
 "RenderType" = "Opaque"
 "RenderPipeline" = "UniversalPipeline"
 }

 Pass
 {
 HLSLPROGRAM
 #pragma vertex Vert
 #pragma fragment Frag

 #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

 struct Attributes
 {
 float3 positionOS : POSITION;
 };

 struct Varyings
 {
 float4 positionCS : SV_POSITION;
 };

 CBUFFER_START(UnityPerMaterial)
 float4 _BaseColor;
 CBUFFER_END

 Varyings Vert(Attributes input)
 {
 Varyings output;
 output.positionCS = TransformObjectToHClip(input.positionOS);
 return output;
 }

 half4 Frag(Varyings input) : SV_Target
 {
 return _BaseColor;
 }
 ENDHLSL
 }
 }
}

В блоке Properties описывается параметр, видимый в инспекторе. Его имя должно совпадать с переменной в HLSL.

Блок UnityPerMaterial используется для размещения параметров материала в постоянном буфере. Такой формат важен для совместимости с SRP Batcher и уменьшения стоимости переключения между материалами.

Структура Attributes содержит данные, поступающие от меша. Семантика POSITION означает позицию вершины. В структуре Varyings описывается информация, передаваемая из вершинной функции во фрагментную. Семантика SV_POSITION указывает на итоговую позицию вершины в экранном пространстве.

Функция TransformObjectToHClip преобразует координаты из локального пространства сразу в однородное клиповое пространство. Это удобная функция URP, которая скрывает последовательное умножение на матрицы объекта, камеры и проекции.

Добавление UV-координат и текстуры

Большинство материалов используют текстуры. Чтобы получить изображение на поверхности, необходимо передать UV-координаты из меша во фрагментную программу, объявить текстуру и сэмплер, а затем выполнить выборку с помощью функций URP.

Пример ниже показывает базовый текстурный материал с цветовым множителем.

Properties
{
 _BaseMap ("Base Map", 2D) = "white" {}
 _BaseColor ("Base Color", Color) = (1, 1, 1, 1)
}

HLSLPROGRAM

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);

CBUFFER_START(UnityPerMaterial)
 float4 _BaseColor;
 float4 _BaseMap_ST;
CBUFFER_END

struct Attributes
{
 float3 positionOS : POSITION;
 float2 uv : TEXCOORD0;
};

struct Varyings
{
 float4 positionCS : SV_POSITION;
 float2 uv : TEXCOORD0;
};

Varyings Vert(Attributes input)
{
 Varyings output;
 output.positionCS = TransformObjectToHClip(input.positionOS);
 output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
 return output;
}

half4 Frag(Varyings input) : SV_Target
{
 half4 texel = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
 return texel * _BaseColor;
}

Параметр _BaseMap_ST содержит масштаб и смещение текстуры. Макрос TRANSFORM_TEX применяет эти значения к UV-координатам. Если не использовать его, настройки Tiling и Offset в материале не будут работать ожидаемым образом.

Координаты UV обычно интерполируются между вершинами. На крупных треугольниках это экономит вычисления, однако результат зависит от перспективы и плотности сетки.

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

Пространства координат и нормали

Одна из самых частых причин ошибок в шейдерах - смешение пространств координат. Локальное пространство относится к модели, мировое - ко всей сцене, view-пространство связано с камерой, а clip-пространство используется для подготовки вершины к растеризации.

Позицию объекта обычно преобразуют из Object Space в World Space, а затем в Clip Space.

Нормаль нельзя преобразовывать простой матрицей положения, поскольку она является направлением и должна сохранять корректное перпендикулярное отношение к поверхности. Особенно заметна проблема при неравномерном масштабировании объекта.

Для преобразования нормали URP предоставляет функции, учитывающие матрицу нормалей. Упрощённый вариант кода может выглядеть так:

struct Attributes
{
 float3 positionOS : POSITION;
 float3 normalOS : NORMAL;
};

struct Varyings
{
 float4 positionCS : SV_POSITION;
 float3 normalWS : TEXCOORD0;
};

Varyings Vert(Attributes input)
{
 Varyings output;
 output.positionCS = TransformObjectToHClip(input.positionOS);
 output.normalWS = TransformObjectToWorldNormal(input.normalOS);
 return output;
}

half4 Frag(Varyings input) : SV_Target
{
 float3 normalWS = normalize(input.normalWS);
 float lightValue = saturate(dot(normalWS, float3(0, 1, 0)));
 return half4(lightValue, lightValue, lightValue, 1);
}

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

Операция saturate ограничивает результат диапазоном от нуля до единицы.

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

Работа с главным источником света

Ручное освещение в URP обычно строится вокруг функций из библиотек Universal Render Pipeline. Они позволяют получить параметры главного источника света, включая цвет, направление, расстояние до источника и данные о затенении.

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

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

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

half4 Frag(Varyings input) : SV_Target
{
 float3 normalWS = normalize(input.normalWS);
 Light mainLight = GetMainLight();

 float diffuse = saturate(dot(normalWS, mainLight.direction));
 half3 color = _BaseColor.rgb * mainLight.color * diffuse;

 return half4(color, _BaseColor.a);
}

Такой пример является минимальной моделью диффузного освещения. В реальном материале следует учитывать ambient lighting, тени, дополнительные источники, emission и настройки окружающей среды.

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

При разработке высокотехнологичных визуализаций часто применяют стилизованное освещение: ступенчатое распределение яркости, усиленный контраст, цветной rim light или зависимость цвета от угла обзора.

Важно отделять художественный эффект от физически корректной модели и давать художнику понятные параметры управления.

Тени и дополнительные источники света

Поддержка теней в собственном шейдере требует не только вычисления основного света. Материал должен уметь получать значение shadow attenuation и учитывать его в итоговом освещении. Без этого объект может оставаться одинаково ярким в тени и на открытом участке сцены.

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

В некоторых проектах это оправданно, а в других выгоднее отключить тени для мелких декоративных объектов.

Дополнительные источники света могут обрабатываться в отдельном проходе или через циклы, предоставляемые URP. Их стоимость зависит от настроек pipeline, платформы и количества пикселей, на которые влияет источник.

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

Для VR и мобильных устройств особенно важно контролировать количество источников, влияющих на один объект.

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

Модель освещения и эффект глянца

Простейшая модель освещения использует диффузный компонент, зависящий от угла между нормалью и направлением света. Для глянцевого блика добавляется specular-компонент.

Один из классических вариантов строится на половинном векторе между направлением света и направлением на камеру.

float3 normalWS = normalize(input.normalWS);
float3 viewDirWS = normalize(_WorldSpaceCameraPos - input.positionWS);
float3 halfDir = normalize(mainLight.direction + viewDirWS);

float diffuse = saturate(dot(normalWS, mainLight.direction));
float specular = pow(
 saturate(dot(normalWS, halfDir)),
 _Smoothness * 128.0
);

half3 result = _BaseColor.rgb * diffuse;
result += specular * _SpecularColor.rgb;

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

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

Для реалистичных материалов предпочтительно использовать физически основанные модели URP, поскольку они согласованно обрабатывают roughness, metallic, Fresnel и энергию света.

Ручная модель полезна для особых художественных стилей: интерфейсных панелей, энергетических щитов, неоновых объектов и научно-фантастических поверхностей.

Материалы PBR в URP

Physically Based Rendering использует параметры, которые ближе к реальным свойствам поверхности. Наиболее распространены базовый цвет, metallic, smoothness или roughness, normal map и occlusion. Комбинация этих данных позволяет одному материалу правдоподобно выглядеть при разных условиях освещения.

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

Smoothness контролирует ширину и интенсивность блика: гладкая поверхность отражает свет узким ярким пятном, шероховатая распределяет его шире.

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

Иначе результат может отличаться от стандартных материалов Unity и выглядеть неестественно.

Если нет необходимости менять внутреннюю модель освещения, часто рациональнее расширить стандартный URP Lit Shader через Shader Graph.

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

Прозрачность и альфа-тестирование

Прозрачный шейдер отличается от непрозрачного настройками RenderQueue, Blend, ZWrite и RenderType. В простом варианте цвет смешивается с фоном по альфа-каналу. Однако прозрачные объекты требуют сортировки, поэтому порядок их отображения не всегда совпадает с геометрической глубиной.

Tags
{
 "RenderType" = "Transparent"
 "Queue" = "Transparent"
}

Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off

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

Для стекла, интерфейсных панелей и дымки это обычно приемлемо. Для плотной листвы, сеток и технических деталей часто лучше использовать альфа-тестирование.

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

AlphaToMask Off

half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
clip(color.a - _Cutoff);
return color;

Прозрачность особенно затратна в сценах с большим количеством перекрывающихся слоёв.

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

Эмиссия, неон и голографические эффекты

Эмиссия добавляет к материалу цвет, который не зависит от прямого освещения. Это основа для неоновых линий, светящихся панелей, индикаторов, энергетических полей и интерфейсных элементов.

Сам шейдер не создаёт ореол автоматически, но яркий цвет может использоваться системой Bloom в постобработке.

Properties
{
 _EmissionColor ("Emission Color", Color) = (0, 1, 1, 1)
 _EmissionPower ("Emission Power", Range(0, 20)) = 3
}

half3 emission = _EmissionColor.rgb * _EmissionPower;
half3 finalColor = baseColor + emission;

Для голограммы можно использовать зависимость от угла между нормалью и направлением взгляда. Когда поверхность наблюдается под острым углом, Fresnel-коэффициент увеличивается, и края объекта становятся ярче.

float3 normalWS = normalize(input.normalWS);
float3 viewDirWS = normalize(_WorldSpaceCameraPos - input.positionWS);
float fresnel = pow(
 1.0 - saturate(dot(normalWS, viewDirWS)),
 _FresnelPower
);

half3 holo = _HologramColor.rgb * fresnel;
return half4(baseColor + holo, alpha);

Добавив движущуюся полоску, можно получить эффект сканирования. Для этого используются мировая позиция или UV-координаты, умноженные на время.

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

Технический стиль Hi-Tech хорошо поддерживается сочетанием холодных цветов, тонкой эмиссии, контрастного Fresnel-края и маски сетки. Чтобы поверхность не превратилась в бесформенное свечение, базовый цвет и эмиссию желательно настраивать раздельно.

Работа со временем и анимацией UV

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

float2 animatedUV = input.uv;
animatedUV += _Time.y * _ScrollSpeed.xy;

half4 pattern = SAMPLE_TEXTURE2D(
 _PatternTex,
 sampler_PatternTex,
 animatedUV
);

В Unity переменная _Time содержит несколько вариантов времени, включая значение в секундах. Для плавного движения обычно используется _Time.y.

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

Для повторяемого эффекта можно использовать frac, которая возвращает дробную часть числа. Это удобно для циклической полосы или повторяющегося импульса, но резкий переход в точке единицы иногда заметен. Сгладить его помогают smoothstep и правильно подобранная маска.

Анимация UV почти бесплатна с точки зрения геометрии, но текстурные выборки и сложные операции остаются.

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

Процедурные узоры и шум

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

Например, сетку можно получить, используя дробную часть координат и расстояние до границы клетки. Чем ближе координата к нулю или единице, тем ярче линия. Дополнительная ширина регулируется параметром.

float2 cell = abs(frac(input.uv) - 0.5);
float line = 1.0 - step(_LineWidth, min(cell.x, cell.y));
half3 gridColor = line * _GridColor.rgb;

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

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

Оптимальный вариант определяется профилированием.

Вершинные эффекты и деформация геометрии

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

Если сетка слишком редкая, деформация будет угловатой.

float3 positionWS = TransformObjectToWorld(input.positionOS);
positionWS.y += sin(
 positionWS.x * _WaveFrequency +
 _Time.y * _WaveSpeed
) * _WaveAmplitude;

output.positionCS = TransformWorldToHClip(positionWS);

При деформации необходимо подумать о нормалях. Если объект изгибается, старая нормаль может больше не соответствовать поверхности, и освещение станет неправильным.

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

Изменение вершин на GPU снижает нагрузку на CPU и позволяет обрабатывать большое количество объектов. Но оно не заменяет систему физики, коллизий или логики игры. Визуальная деформация не изменяет исходную форму коллайдера, если это не предусмотрено отдельно.

Многопроходные шейдеры

Pass описывает отдельный этап отрисовки объекта. Один проход может отвечать за базовый цвет, другой - за контур, третий - за тени или дополнительный свет. Многопроходность расширяет возможности, но каждый проход увеличивает стоимость отрисовки.

Контур можно реализовать, расширив вершины по нормали и отрисовав их обратными сторонами однотонным цветом.

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

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

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

Варианты шейдеров и условная компиляция

URP поддерживает множество вариантов одного шейдера. Они создаются из-за включённых и выключенных функций: теней, дополнительных источников, смешивания света, альфа-тестирования и других возможностей. Каждый вариант может увеличивать время компиляции и размер сборки.

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

На практике часть вариантов не используется, но сам принцип показывает, почему без контроля проект быстро сталкивается с проблемой shader variant explosion.

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

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

Варианты особенно важны для мобильных проектов и Web-платформ. Долгий прогрев шейдеров проявляется как рывки при первом появлении эффекта. Предварительная компиляция, Shader Variant Collection и тестирование на реальном устройстве помогают уменьшить такие проблемы.

SRP Batcher и совместимость материалов

SRP Batcher уменьшает стоимость подготовки команд отрисовки на центральном процессоре. Чтобы материал мог эффективно работать с ним, параметры должны быть организованы в совместимом постоянном буфере, обычно через UnityPerMaterial.

Несовпадение структуры буферов между проходами, использование неподдерживаемых типов данных или неправильное размещение параметров может лишить шейдер преимуществ пакетной обработки.

Поэтому при создании собственного материала необходимо проверять статус совместимости в инспекторе и анализировать результат в профайлере.

SRP Batcher не ускоряет сам фрагментный код. Если шейдер выполняет сложные вычисления на миллионах пикселей, оптимизация только CPU-части не решит проблему. Это инструмент для снижения накладных расходов подготовки, а не универсальное средство ускорения любой графики.

GPU Instancing решает другую задачу: позволяет рисовать множество экземпляров с одной геометрией, изменяя отдельные параметры. В сочетании с SRP Batcher нужно учитывать особенности проекта и конкретного материала.

Иногда инстансинг выгоднее для большого числа одинаковых объектов, а иногда батчер обеспечивает лучший результат.

Оптимизация фрагментного шейдера

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

Первый принцип оптимизации - не вычислять то, что не влияет на результат.

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

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

Однако конкретное поведение зависит от графического процессора и версии компилятора.

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

Профилирование должно проводиться в реальном разрешении и на целевой платформе.

Приём Потенциальная польза Возможный риск
Удаление лишних выборок Снижение нагрузки на память и фрагментный блок Потеря детализации материала
Использование half Меньше затрат на вычисления на совместимых GPU Артефакты из-за недостаточной точности
Перенос вычислений в vertex Меньше работы на каждый пиксель Потеря точности на крупных треугольниках
Отключение дополнительных источников Снижение количества световых операций Менее реалистичное освещение
Сокращение вариантов Меньше размер сборки и время загрузки Меньше гибкости функций

Отладка визуальных ошибок

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

Розовый цвет в Unity обычно означает ошибку компиляции или несовместимость шейдера.

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

Для анализа промежуточных данных временно выводите их напрямую в цвет. Например, мировую нормаль можно отобразить как color = normal * 0.5 + 0.5, UV - как float4(uv, 0, 1), а значение маски - как одинаковое значение в трёх цветовых каналах.

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

Если эффект зависит от камеры, проверьте направление viewDir и положение камеры в мировом пространстве. Нередко проблема возникает из-за того, что одно направление направлено от поверхности к камере, а другое - наоборот. Смена знака меняет результат Fresnel, блика и отражения.

Цветовое пространство и HDR

Unity может работать в Gamma или Linear Color Space. Для освещения и физических вычислений обычно предпочтительно линейное пространство, поскольку сложение и умножение света в нём ближе к математически корректному поведению.

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

Эмиссия способна использовать значения выше единицы, особенно если материал должен вызвать Bloom. Такой цвет называют HDR-значением.

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

Текстурные данные имеют разные типы. Базовый цвет обычно импортируется как цветовая текстура с преобразованием, а маски roughness, metallic и occlusion часто должны интерпретироваться как линейные данные. Неправильная настройка импорта способна изменить физический смысл каналов.

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

Подход к созданию шейдера от идеи до готового материала

Начинайте с формулировки визуальной задачи. Вместо фразы "нужен сложный футуристичный шейдер" лучше определить конкретные свойства: поверхность должна светиться по краям, иметь движущуюся сетку, реагировать на расстояние до камеры и сохранять тени.

Затем создайте минимальный прототип. Сначала выводите базовый цвет, после этого добавляйте UV, нормали, освещение, маски и анимацию.

Такой порядок позволяет определить, какая часть кода отвечает за конкретный эффект. Если собрать всё сразу, поиск причины ошибки может занять значительно больше времени.

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

Финальный этап - очистка параметров и документация. Удалите временные переменные, переименуйте свойства понятными словами, задайте диапазоны и добавьте краткие описания.

Хорошо оформленный шейдер быстрее настраивается художниками и реже ломается при передаче между специалистами.

Пример архитектуры технологического материала

Рассмотрим материал для интерактивной панели в стиле Hi-Tech. Он может включать базовую текстуру, цвет панели, эмиссию, движущуюся сетку, Fresnel-край и маску повреждений.

Каждый компонент желательно сделать независимым, чтобы художник мог отключить его или изменить интенсивность.

Базовый цвет отвечает за корпус панели. Сетка использует отдельную маску с повторяющимися UV. Сканирующая полоса вычисляется по мировой координате или второй UV-развёртке. Fresnel добавляет подсветку по краям, а эмиссия объединяет сетку и полосу перед выводом результата.

Для интерактивности можно передавать в материал значение прогресса операции: например, заряд аккумулятора, уровень сигнала или состояние системы. Это значение используется как порог для маски и позволяет связать визуальный эффект с логикой приложения.

float scan = smoothstep(
 _ScanPosition - _ScanWidth,
 _ScanPosition,
 input.positionWS.y
);

float gridValue = SAMPLE_TEXTURE2D(
 _GridTex,
 sampler_GridTex,
 input.uv * _GridTiling
).r;

float fresnel = pow(
 1.0 - saturate(dot(normalWS, viewDirWS)),
 _FresnelPower
);

half3 emission =
 _GridColor.rgb * gridValue * _GridIntensity +
 _ScanColor.rgb * scan * _ScanIntensity +
 _EdgeColor.rgb * fresnel * _EdgeIntensity;

Подобная архитектура удобна тем, что каждая часть может тестироваться отдельно.

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

Профилирование и измерение результата

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

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

Если узким местом является CPU, проверьте число объектов, переключения материалов, количество проходов и совместимость с SRP Batcher.

Frame Debugger помогает увидеть, какие команды были отправлены, какие Pass использованы и почему объект отрисовывается несколько раз. Профайлер GPU показывает время отдельных этапов, но интерпретировать данные нужно с учётом особенностей конкретного устройства.

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

Типичные ошибки начинающих разработчиков

Первая ошибка - копирование кода из старых встроенных шейдеров Unity без адаптации к URP. Built-in Render Pipeline и Universal Render Pipeline используют разные библиотеки, теги, функции освещения и структуру проходов. Старый пример может не компилироваться или работать без теней.

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

Третья ошибка - использование float для всех переменных без анализа необходимости. Это не всегда критично, но при сложном материале и мобильной цели может быть неэффективно. С другой стороны, бездумная замена всех типов на half способна создать мерцание или полосы.

Четвёртая ошибка - отсутствие тестов на прозрачность, резкий масштаб объекта и разные углы обзора. Материал может выглядеть идеально в одном положении, но ломаться при пересечении с другими объектами или при работе камеры с большим диапазоном глубины.

Рекомендации для командной разработки

Храните шейдеры, Shader Graph и связанные HLSL-файлы в понятной структуре каталогов. Имена должны отражать назначение материала и его уровень: базовый, эффектный, UI, постобработка или экспериментальный.

Это упрощает поиск и снижает вероятность случайного использования тестового варианта в рабочей сцене.

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

Согласуйте правила именования свойств. Например, _BaseColor, _BaseMap, _EmissionColor и _EmissionPower понятнее, чем случайные короткие названия. Единый стиль облегчает переход между Shader Graph и HLSL и делает автоматическую настройку материалов более предсказуемой.

Перед выпуском проекта фиксируйте список поддерживаемых платформ и проверяйте шейдеры на каждой из них. Настольный DirectX, мобильный Vulkan, Metal и WebGL могут по-разному обрабатывать точность, производительность и доступные функции. Универсальность URP не отменяет необходимости реального тестирования.

План обучения разработке шейдеров

Начать лучше с математики цвета и векторов: сложение, умножение, нормализация, dot, cross, lerp, smoothstep и saturate. Эти операции составляют основу большинства материалов. Понимание их геометрического смысла полезнее, чем механическое запоминание готовых фрагментов кода.

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

Shader Graph помогает визуализировать взаимосвязи, а HLSL закрепляет понимание того, что происходит на уровне GPU.

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

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

Нужно ли изучать HLSL, если используется Shader Graph?

Для первых эффектов HLSL не обязателен. Однако базовые знания языка помогают понимать стоимость узлов, исправлять ошибки, создавать Custom Function и оптимизировать итоговый материал. Shader Graph не отменяет графическую математику, а только представляет её в визуальной форме.

Почему собственный шейдер выглядит темнее стандартного Lit?

Чаще всего отсутствуют окружающее освещение, правильная обработка теней, отражения или корректное цветовое пространство. Упрощённый диффузный расчёт учитывает только один источник и поэтому не может полностью повторить стандартную модель URP.

Что быстрее: процедурный узор или текстура?

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

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

Можно ли использовать один шейдер для всех платформ?

Да, но универсальный код обычно требует компромиссов. Для мобильных устройств, VR и настольных систем могут понадобиться разные уровни качества, варианты освещения и ограничения по прозрачности. Лучше заранее предусмотреть упрощённые режимы, чем пытаться заставить один тяжёлый материал работать одинаково эффективно везде.

Собственные шейдеры для Unity URP открывают доступ к ключевому уровню современной визуализации: разработчик управляет не только цветом объекта, но и логикой его взаимодействия со светом, камерой, глубиной и временем.

Начинать стоит с минимальных материалов, постепенно добавляя текстуры, нормали, освещение, прозрачность и процедурные эффекты.

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

Эффект должен быть выразительным, но не перегружать фрагментный шейдер; материал должен быть настраиваемым, но не порождать тысячи ненужных вариантов; код должен использовать возможности URP, но оставаться понятным для команды.

Если сочетать Shader Graph для быстрых художественных итераций, HLSL для точного контроля, профилирование для проверки гипотез и тестирование на реальных устройствах, URP становится не ограничением, а полноценной основой для создания современных Hi-Tech-интерфейсов, интерактивных визуализаций и высокотехнологичной графики.