Engine & platform

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

CHRISTOPHE RICCIO / UNITY TECHNOLOGIESContributor
May 14, 2018|12 Мин
Удаление скриптовых вариантов шейдеров
Эта веб-страница была переведена с помощью машинного перевода для вашего удобства. Мы не можем гарантировать точность или надежность переведенного контента. Если у вас есть вопросы о точности переведенного контента, обращайтесь к официальной английской версии веб-страницы.

Значительно сократите время сборки плеера и размер данных, позволив разработчикам контролировать, какие варианты шейдеров будут обрабатываться компилятором 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 в Настройках графики.

Опции автоматического удаления вариантов шейдеров в GraphicsSettings.
Опции автоматического удаления вариантов шейдеров в GraphicsSettings.

Автоматическое удаление вариантов шейдеров основано на ограничениях времени сборки. 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#, и каждый обратный вызов выполняется для каждого фрагмента шейдера.

API зачистки вариантов шейдеров с помощью сценариев

В качестве примера, следующий скрипт позволяет удалить все варианты шейдеров, которые будут связаны с конфигурацией "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
creenshot демонстрации фотограмметрии Фонтенбло с использованием конвейера HD Render Pipeline со стандартного разрешения PlayStation 4 1920x1080.
creenshot демонстрации фотограмметрии Фонтенбло с использованием конвейера HD Render Pipeline со стандартного разрешения PlayStation 4 1920x1080.

Более того, конвейер рендеринга 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

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

Скриншот демонстрации конвейера Lightweight.
Скриншот демонстрации конвейера Lightweight.
Советы по написанию кода зачистки вариантов шейдеров
Улучшение дизайна кода шейдеров

Проект может быстро столкнуться с взрывом количества вариантов шейдеров, что приведет к непосильному времени компиляции и увеличению размера данных плеера. Сценарий зачистки шейдеров помогает решить эту проблему, но Вам следует пересмотреть то, как Вы используете ключевые слова шейдеров, чтобы генерировать более релевантные варианты шейдеров. Мы можем положиться на #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_ORANGE + OP_ADD, COLOR_VIOLET + OP_MUL, COLOR_GREEN + OP_MUL.
COLOR_ORANGE + OP_ADD, COLOR_VIOLET + OP_MUL, COLOR_GREEN + OP_MUL.

Во-первых, мы должны убедиться, что каждое ключевое слово действительно полезно. В этой сцене 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. Конвейер сборки шейдеров будет выполнять обратные вызовы в порядке возрастания CallbackOrder, то есть сначала самый низкий, а потом самый высокий.

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

  • Сценарий 1: Систематически удаляет все варианты шейдеров с неверными путями кода.
  • Сценарий 2: Удаляет все варианты отладочных шейдеров.
Варианты
  • Сценарий 3: Удалите из кодовой базы все варианты шейдеров, которые не нужны для текущего проекта.
  • Сценарий 4: Зарегистрируйте оставшиеся варианты шейдеров и удалите их все, чтобы ускорить время итерации скриптов удаления.
Процесс написания скрипта для снятия вариантов шейдеров

Снятие вариантов шейдеров является чрезвычайно мощным средством, но требует много работы для достижения хороших результатов.

В представлении проекта отфильтруйте все шейдеры.

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

Убедитесь, что Вы знаете, какие специфические графические функции использует проект.

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

Уберите варианты шейдеров из сценария.

Проверьте визуальные эффекты в сборке.

Повторите шаги 2 - 6 для каждого шейдера.

Загрузите пример проекта

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

Узнайте больше об оптимизации размера двоичных развертываний на Unite Berlin

Приходите на выступление Йонаса Эхтерхоффа 21 июня и узнайте обо всех новых инструментах, которые дают Вам больше контроля над тем, что попадает в Вашу сборку!