Удаление скриптовых вариантов шейдеров

Значительно сократите время сборки плеера и размер данных, позволив разработчикам контролировать, какие варианты шейдеров будут обрабатываться компилятором Unity Shader и включаться в данные плеера.
Время сборки плеера и размер данных увеличиваются вместе со сложностью Вашего проекта из-за растущего числа вариантов шейдеров.
С помощью скриптового удаления вариантов шейдеров, представленного в бета-версии 2018.2, Вы можете управлять количеством генерируемых вариантов шейдеров и, таким образом, значительно сократить время сборки Player и размер данных.
Эта функция позволяет Вам удалить все варианты шейдеров с недействительными путями кода, удалить варианты шейдеров для неиспользуемых функций или создать конфигурации сборки шейдеров, такие как "debug" и "release", не влияя на время итерации или сложность обслуживания.
В этой статье мы сначала дадим определение некоторым терминам, которые мы используем. Затем мы сосредоточимся на определении вариантов шейдеров, чтобы объяснить, почему мы можем генерировать их так много. Далее следует описание автоматической зачистки вариантов шейдеров и того, как скриптовая зачистка вариантов шейдеров реализована в архитектуре шейдерного конвейера Unity. Затем мы рассмотрим API зачистки вариантов шейдеров с помощью скриптов, обсудим результаты на демонстрационном примере Fountainbleau и в заключение дадим несколько советов по написанию скриптов зачистки.
Освоение скриптового отсечения вариантов шейдеров - это не тривиальная задача, но она может привести к значительному повышению эффективности работы команды!
Чтобы понять функцию зачистки вариантов шейдеров с помощью сценариев, важно иметь точное представление о различных концепциях.
- Шейдерный актив: Полный исходный код файла со свойствами, субшейдерами, передачами и HLSL.
- Фрагмент шейдера: Входной код HLSL с зависимостями для одной стадии шейдера.
- Стадия шейдера: Определенный этап в конвейере рендеринга GPU, обычно это этап вершинного шейдера и фрагментного шейдера.
- Ключевое слово Shader: Идентификатор препроцессора для ветвлений между шейдерами во время компиляции.
- Набор ключевых слов шейдера: Определенный набор ключевых слов шейдера, идентифицирующий конкретный путь кода.
- Вариант шейдера: Код шейдера для конкретной платформы, созданный компилятором шейдеров Unity, для одного этапа шейдера для определенного графического уровня, прохода, набора ключевых слов шейдера и т.д.
- Uber shader: Источник шейдеров, который может создавать множество вариантов шейдеров.
В Unity uber-шейдеры управляются подшейдерами, проходами и типами шейдеров ShaderLab, а также директивами препроцессора#pragma multi_compile и #pragma shader_feature.
Чтобы использовать скриптовое удаление вариантов шейдеров, Вам необходимо четко понимать, что такое вариант шейдера и как варианты шейдеров генерируются конвейером сборки шейдеров. Количество генерируемых вариантов шейдеров прямо пропорционально времени сборки и размеру данных варианта шейдера Player. Вариант шейдера - это один из результатов конвейера сборки шейдеров.
Ключевые слова шейдеров - это один из элементов, вызывающих генерацию вариантов шейдеров. Непродуманное использование ключевых слов шейдеров может быстро привести к взрыву количества вариантов шейдеров и, как следствие, к очень долгому времени сборки.
Чтобы увидеть, как генерируются варианты шейдеров, следующий простой шейдер подсчитает, сколько вариантов шейдеров он производит:
Shader "ShaderVariantsStripping"
{
SubShader
{
Pass
{
Name "ShaderVariantsStripping/Pass"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile COLOR_ORANGE COLOR_VIOLET COLOR_GREEN COLOR_GRAY
#pragma multi_compile OP_ADD OP_MUL OP_SUB
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 get_color()
{
#if defined(COLOR_ORANGE)
return fixed4(1.0, 0.5, 0.0, 1.0);
#elif defined(COLOR_VIOLET)
return fixed4(0.8, 0.2, 0.8, 1.0);
#elif defined(COLOR_GREEN)
return fixed4(0.5, 0.9, 0.3, 1.0);
#elif defined(COLOR_GRAY)
return fixed4(0.5, 0.9, 0.3, 1.0);
#else
#error "Unknown 'color' keyword"
#endif
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 diffuse = tex2D(_MainTex, i.uv);
fixed4 color = get_color();
#if defined(OP_ADD)
return diffuse + color;
#elif defined(OP_MUL)
return diffuse * color;
#elif defined(OP_SUB)
return diffuse - color;
#else
#error "Unknown 'op' keyword"
#endif
}
ENDCG
}
}
}
Общее количество вариантов шейдеров в проекте детерминировано и определяется следующим уравнением:

Следующий тривиальный пример ShaderVariantStripping вносит ясность в это уравнение. Это один шейдер, который упрощает уравнение следующим образом:

Аналогично, этот шейдер имеет один субшейдер и один проход, что еще больше упрощает уравнение:

Ключевые слова в уравнении относятся как к платформе, так и к ключевым словам шейдера. Графический уровень - это определенная комбинация набора ключевых слов для платформы.
В ShaderVariantStripping/Pass есть две директивы мультикомпиляции. Первая директива определяет 4 ключевых слова (COLOR_ORANGE, COLOR_VIOLET, COLOR_GREEN, COLOR_GRAY), а вторая директива определяет 3 ключевых слова (OP_ADD, OP_MUL, OP_SUB). Наконец, прохождение определяет 2 этапа шейдера: этап вершинного шейдера и этап фрагментного шейдера.
Общее количество вариантов шейдеров указано для одного поддерживаемого графического API. Однако для каждого поддерживаемого в проекте графического API нам нужен отдельный набор вариантов шейдеров. Например, если мы создадим Android Player, поддерживающий OpenGL ES 3 и Vulkan, нам понадобится два набора вариантов шейдеров. В результате время сборки плеера и размер шейдерных данных прямо пропорциональны количеству поддерживаемых графических API.
Конвейер компиляции шейдеров в Unity - это черный ящик, в котором каждый шейдер в проекте разбирается для извлечения фрагментов шейдеров перед сбором инструкций предварительной обработки, таких как multi_compile и shader_feature. Получается список параметров компиляции, по одному на каждый вариант шейдера.
Эти параметры компиляции включают фрагмент шейдера, графический уровень, тип шейдера, набор ключевых слов шейдера, тип и имя передачи. Каждый из заданных параметров компиляции используется для создания одного варианта шейдера.
Следовательно, Unity выполняет автоматическую зачистку вариантов шейдеров, основываясь на двух эвристиках. Во-первых, отсечение основано на настройках проекта, например, если отключена поддержка виртуальной реальности, то варианты шейдеров VR систематически отсекаются. Во-вторых, автоматическое снятие основывается на конфигурации раздела Shader Stripping в Настройках графики.

Автоматическое удаление вариантов шейдеров основано на ограничениях времени сборки. Unity не может автоматически выбрать только необходимые варианты шейдеров во время сборки, потому что эти варианты шейдеров зависят от выполнения C# во время исполнения. Например, если скрипт C# добавляет точечный свет, но на момент сборки не было ни одного точечного света, то конвейер сборки шейдеров никак не сможет определить, что Игроку нужен вариант шейдера, который делает затенение точечного света.
Вот список вариантов шейдеров с включенными ключевыми словами, которые автоматически удаляются:
Режимы Lightmap: LIGHTMAP_ON, DIRLIGHTMAP_COMBINED, DYNAMICLIGHTMAP_ON, LIGHTMAP_SHADOW_MIXING, SHADOWS_SHADOWMASK
Режимы тумана: FOG_LINEAR, FOG_EXP, FOG_EXP2
Варианты инстанций: INSTANCING_ON
Кроме того, когда поддержка виртуальной реальности отключена, варианты шейдеров со следующими встроенными ключевыми словами будут удалены:
STEREO_INSTANCING_ON, STEREO_MULTIVIEW_ON, STEREO_CUBEMAP_RENDER_ON, UNITY_SINGLE_PASS_STEREO
Когда автоматическая очистка выполнена, конвейер сборки шейдеров использует оставшиеся наборы параметров компиляции для параллельного планирования компиляции вариантов шейдеров, запуская столько одновременных компиляций, сколько потоков ядра процессора имеется в платформе.
Вот визуальное представление этого процесса:

В Unity 2018.2 beta архитектура конвейера шейдеров представляет новый этап непосредственно перед планированием компиляции вариантов шейдеров, позволяя пользователям контролировать компиляцию вариантов шейдеров. Эта новая стадия передается пользовательскому коду через обратные вызовы C#, и каждый обратный вызов выполняется для каждого фрагмента шейдера.
В качестве примера, следующий скрипт позволяет удалить все варианты шейдеров, которые будут связаны с конфигурацией "DEBUG", идентифицированной ключевым словом "DEBUG", используемым в сборке Development Player.
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;
// Simple example of stripping of a debug build configuration
class ShaderDebugBuildProcessor : IPreprocessShaders
{
ShaderKeyword m_KeywordDebug;
public ShaderDebugBuildProcessor()
{
m_KeywordDebug = new ShaderKeyword("DEBUG");
}
// Multiple callback may be implemented.
// The first one executed is the one where callbackOrder is returning the smallest number.
public int callbackOrder { get { return 0; } }
public void OnProcessShader(
Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> shaderCompilerData)
{
// In development, don't strip debug variants
if (EditorUserBuildSettings.development)
return;
for (int i = 0; i < shaderCompilerData.Count; ++i)
{
if (shaderCompilerData[i].shaderKeywordSet.IsEnabled(m_KeywordDebug))
{
shaderCompilerData.RemoveAt(i);
--i;
}
}
}
}
OnProcessShader вызывается непосредственно перед планированием компиляции варианта шейдера.
Каждая комбинация экземпляров Shader, ShaderSnippetData и ShaderCompilerData - это идентификатор одного варианта шейдера, который будет создан компилятором шейдеров. Чтобы убрать этот вариант шейдера, нам нужно только удалить его из списка ShaderCompilerData.
Каждый вариант шейдера, который компилятор шейдеров должен сгенерировать, появится в этом обратном вызове. При работе над сценарием удаления вариантов шейдеров Вам нужно сначала выяснить, какие варианты нужно удалить, потому что они не нужны проекту.
Один из вариантов использования скриптовой зачистки вариантов шейдеров - систематическое удаление недействительных вариантов шейдеров из конвейера рендеринга из-за различных комбинаций ключевых слов шейдеров.
Скрипт удаления вариантов шейдеров, включенный в конвейер рендеринга HD, позволяет Вам систематически сокращать время сборки и размер проекта, используя конвейер рендеринга HD. Этот сценарий применяется к следующим шейдерам:
HDRenderPipeline/Lit
HDRenderPipeline/LitTessellation
HDRenderPipeline/LayeredLit
HDRenderPipeline/LayeredLitTessellation
Сценарий выдает следующие результаты:
Unstripped Stripped
Player Data Shader Variant Count 24350 (100%) 12122 (49.8%)
Player Data Size on disk 511 MB 151 MB
Player Build Time 4864 seconds 1356 seconds

Более того, конвейер рендеринга Lightweight для Unity 2018.2 имеет пользовательский интерфейс для автоматической подачи скрипта зачистки, который может автоматически зачистить до 98% вариантов шейдеров, что, как мы ожидаем, будет особенно ценно для мобильных проектов.
Другой вариант использования - скрипт для удаления всех функций конвейера рендеринга, которые не используются для конкретного проекта. Используя внутреннюю тестовую демонстрацию для конвейера рендеринга Lightweight, мы получили следующие результаты для всего проекта:
Unstripped Stripped
Player Data Shader Variant Count 31080 7056
Player Data Size on disk 121 116
Player Build Time 839 seconds 286 seconds
Как мы видим, использование скриптового отсечения вариантов шейдеров может привести к значительным результатам, а если еще больше поработать над скриптом отсечения, то можно пойти еще дальше.

Проект может быстро столкнуться с взрывом количества вариантов шейдеров, что приведет к непосильному времени компиляции и увеличению размера данных плеера. Сценарий зачистки шейдеров помогает решить эту проблему, но Вам следует пересмотреть то, как Вы используете ключевые слова шейдеров, чтобы генерировать более релевантные варианты шейдеров. Мы можем положиться на #pragma skip_variants для проверки неиспользуемых ключевых слов в редакторе.
Например, в ShaderStripping/Color Shader директивы препроцессинга объявляются следующим кодом:
#pragma multi_compile COLOR_ORANGE COLOR_VIOLET COLOR_GREEN COLOR_GRAY // color keywords
#pragma multi_compile OP_ADD OP_MUL OP_SUB // operator keywords
Такой подход подразумевает, что будут сгенерированы все комбинации ключевых слов цвета и ключевых слов оператора.
Допустим, мы хотим изобразить следующую сцену:

Во-первых, мы должны убедиться, что каждое ключевое слово действительно полезно. В этой сцене COLOR_GRAY и OP_SUB никогда не используются. Если мы можем гарантировать, что эти ключевые слова никогда не будут использованы, то нам следует их удалить.
Во-вторых, нам следует комбинировать ключевые слова, которые эффективно создают единый путь кода. В этом примере операция 'add' всегда используется исключительно с цветом 'orange'. Поэтому мы можем объединить их в одно ключевое слово и рефакторить код, как показано ниже.
#pragma multi_compile ADD_COLOR_ORANGE MUL_COLOR_VIOLET MUL_COLOR_GREEN
#if defined(ADD_COLOR_ORANGE)
#define COLOR_ORANGE
#define OP_ADD
#elif defined(MUL_COLOR_VIOLET)
#define COLOR_VIOLET
#define OP_MUL
#elif defined(MUL_COLOR_GREEN)
#define COLOR_GREEN
#define OP_MUL
#endif
Конечно, не всегда есть возможность рефакторинга ключевых слов. В этих случаях зачистка вариантов шейдеров с помощью сценариев - ценный инструмент!
Для каждого фрагмента выполняются все скрипты зачистки вариантов шейдеров. Мы можем упорядочить выполнение скриптов, упорядочив значение, возвращаемое функцией-членом callbackOrder. Конвейер сборки шейдеров будет выполнять обратные вызовы в порядке возрастания CallbackOrder, то есть сначала самый низкий, а потом самый высокий.
Использование нескольких скриптов для снятия шейдеров может быть использовано для разделения скриптов по целям. Например:
- Сценарий 1: Систематически удаляет все варианты шейдеров с неверными путями кода.
- Сценарий 2: Удаляет все варианты отладочных шейдеров.

- Сценарий 3: Удалите из кодовой базы все варианты шейдеров, которые не нужны для текущего проекта.
- Сценарий 4: Зарегистрируйте оставшиеся варианты шейдеров и удалите их все, чтобы ускорить время итерации скриптов удаления.
Снятие вариантов шейдеров является чрезвычайно мощным средством, но требует много работы для достижения хороших результатов.
В представлении проекта отфильтруйте все шейдеры.
Выберите шейдер и в Инспекторе нажмите Показать, чтобы открыть список ключевых слов/вариантов этого шейдера. Будет список ключевых слов, которые всегда включаются в сборку.
Убедитесь, что Вы знаете, какие специфические графические функции использует проект.
Проверьте, используются ли ключевые слова на всех стадиях шейдера. Для этапов, в которых не используются эти ключевые слова, необходим только один вариант.
Уберите варианты шейдеров из сценария.
Проверьте визуальные эффекты в сборке.
Повторите шаги 2 - 6 для каждого шейдера.
Загрузите пример проекта
Пример проекта, использованный для иллюстрации этой статьи в блоге, можно загрузить здесь. Для этого требуется Unity 2018.2.0b1.
Приходите на выступление Йонаса Эхтерхоффа 21 июня и узнайте обо всех новых инструментах, которые дают Вам больше контроля над тем, что попадает в Вашу сборку!