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

ATTILIO CAROTENUTO / UNITYTechnical Lead
May 28, 2024|15 Мин
Оптимизация вариантов шейдеров Unity и советы по устранению неполадок
Эта веб-страница была переведена с помощью машинного перевода для вашего удобства. Мы не можем гарантировать точность или надежность переведенного контента. Если у вас есть вопросы о точности переведенного контента, обращайтесь к официальной английской версии веб-страницы.

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

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

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

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

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

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

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

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

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

Понимание влияния ключевых слов на варианты

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

Чтобы увидеть, какие ключевые слова определены в шейдере, вы можете выбрать его и проверить Инспектор.

Ключевые слова из представления Инспектора шейдеров

Как вы можете видеть, ключевые слова делятся на Переопределяемые и Непереопределяемые. Локальные ключевые слова (те, которые определены в самом файле шейдера) с глобальной областью могут быть переопределены глобальным ключевым словом шейдера с совпадающим именем. Если они определены в локальной области (с помощью multi_compile_local или shader_feature_local), их нельзя переопределить, и они появятся в разделе Непереопределяемые ниже. Глобальные ключевые слова шейдера предоставляются движком Unity, и их можно переопределить. Поскольку их можно добавлять в любой момент в процессе сборки, не все глобальные ключевые слова могут появиться в этом списке.

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

#pragma shader_feature LIGHT_LOW_Q LIGHT_HIGH_Q

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

#ifdef SHADER_API_METAL
   #pragma shader_feature IOS_FOG_FEATURE
#else
   #pragma shader_feature BASE_FOG_FEATURE
#endif

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

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

  • _vertex
  • _fragment
  • _корпус
  • _domain
  • _geometry
  • _рейстрейдинг

Например:

#pragma shader_feature_fragment FRAG_FEATURE_1 FRAG_FEATURE_2

Это может вести себя по-разному в зависимости от используемого рендерера. Например, в OpenGL, OpenGL ES и Vulkan суффиксы будут игнорироваться.

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

Вы также можете дополнительно определить ключевые слова с помощью директивы #pragma dynamic_branch, которая заставит Unity полагаться на динамическую ветвление и не генерировать варианты для этих ключевых слов. Хотя это уменьшает количество полученных вариантов, это может привести к снижению производительности GPU в зависимости от шейдера и контента игры, поэтому рекомендуется профилировать соответствующим образом при его использовании.

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

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

Обычно варианты шейдера не компилируются, пока вы фактически не соберете игру. Используя эту опцию, вы можете просмотреть полученные варианты шейдера для конкретной платформы сборки или графического API. Это позволяет вам заранее проверять наличие ошибок. Кроме того, вы можете вставить сгенерированный код в инструменты анализа производительности шейдеров GPU, такие как PVRShaderEditor, для дальнейшей оптимизации.

Ключевые слова из представления Инспектора шейдеров

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

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

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

Для получения дополнительной информации обратитесь к Проверьте, сколько вариантов шейдеров у вас есть в руководстве Unity.

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

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

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

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

После успешной сборки мы можем заглянуть в файл Editor.log, чтобы собрать полезную информацию о том, какие варианты шейдеров были включены в сборку. Для этого поищите в файле журнала "Компиляция шейдера" и имя вашего шейдера. Вот, например, как это выглядит:

Compiling shader "GameShaders/MyShader" pass "Pass 1" (vp)
	Full variant space:     	608
	After settings filtering:   608
	After built-in stripping:   528
	After scriptable stripping: 528
	Processed in 0.00 seconds
	starting compilation...
	finished in 0.02 seconds. Local cache hits 528 (0.16s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants

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

Если ваша игра поддерживает несколько графических API, вы также найдете информацию для каждого поддерживаемого рендерера:

Serialized binary data for shader GameShaders/MyShader in 0.00s
	gles3 (total internal programs: 290, unique: 193)
	vulkan (total internal programs: 290, unique: 193)

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

Compressed shader 'GameShaders/MyShader' on vulkan from 1.35MB to 0.19MB

Если вы используете универсальный рендер-пайплайн (URP), вы можете выбрать, чтобы журналы генерировались только от шейдеров SRP, от всех шейдеров или отключить журналы. Для этого выберите уровень журнала в Настройках проекта > Графика > Глобальные настройки URP.

Установка уровня журнала в глобальных настройках URP

Кроме того, если вы выберете опцию Экспортировать варианты шейдеров ниже, после сборки будет сгенерирован файл JSON, содержащий отчет о компиляциях вариантов шейдеров. Это доступно в Unity 2022.2 или новее.

Определение, какие варианты используются во время выполнения

Чтобы понять, какие шейдеры фактически компилируются для GPU во время выполнения, вы можете включить опцию Журналировать компиляцию шейдеров в Настройках проекта > Графика.

Включение журнала компиляции шейдеров в настройках проекта графики

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

Формат выглядит так:

Compiled Shader: Folder/ShaderName, pass: PASS_NAME, stage: STAGE_NAME, keywords ACTIVE_KEYWORD_1 ACTIVE_KEYWORD_2

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

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

Обзор шейдеров в Memory Profiler
Удаление на основе настроек графики

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

Чтобы определить их, перейдите в Настройки проекта > Графика. Отсюда, используя встроенный рендеринг, вы можете выбрать, какие режимы Lightmap и Fog поддерживает ваша игра.

Настройки удаления шейдеров графики

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

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

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

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

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

Удаление на основе графических уровней

Примечание. Это актуально только при использовании встроенного конвейера рендеринга. Эти настройки будут игнорироваться при использовании скриптованного конвейера рендеринга, такого как URP.

Графические уровни используются для применения различных графических настроек в зависимости от оборудования, на котором работает ваша игра (не путать с настройками качества). Когда игра запускается, Unity определит графический уровень вашего устройства на основе аппаратных возможностей, графического API и других факторов.

Их можно установить в Настройки проекта > Графика > Настройки уровней.

Настройки графических уровней

На основе этого Unity добавляет эти три ключевых слова ко всем шейдерам:

UNITY_HARDWARE_TIER1

UNITY_HARDWARE_TIER2

UNITY_HARDWARE_TIER3

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

Как уже упоминалось ранее, Unity попытается удалить дубликаты идентичных вариантов, поэтому, если, например, два из трех уровней имеют одинаковые настройки, это приведет к уменьшению размера на диске, даже если все варианты все равно будут сгенерированы. Вы можете дополнительно заставить Unity генерировать варианты уровней для данного шейдера и API графического рендерера, используя hardware_tier_variants, как показано ниже:

// Direct3D 11/12
#pragma hardware_tier_variants d3d11 

Для получения дополнительной информации обратитесь к Графическим уровням в встроенном рендер-пайплайне в руководстве Unity.

Удаление на основе графических API

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

Для этого перейдите в Настройки проекта > Игрок. По умолчанию выбрана Авто графика API, и Unity включит набор встроенных графических API и выберет один во время выполнения в зависимости от возможностей устройства. Например, на Android Unity сначала попытается использовать Vulkan, и если устройство его не поддерживает, движок вернется к GLES3.2, GLES3.1 или GLES3.0 (варианты будут идентичны на этих версиях GLES).

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

Отключите Авто графику API, чтобы выбрать предпочитаемые API

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

Строгое соответствие вариантов шейдеров

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

Начиная с Unity 2022.3, вы можете выбрать Строгое соответствие вариантов шейдеров в Настройках проекта > Игрок, чтобы убедиться, что Unity пытается загрузить только точное соответствие для комбинации локальных и глобальных ключевых слов, которые вам нужны.

Включите строгое соответствие вариантов шейдеров в Настройках проекта

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

Экспорт используемых вариантов в Коллекцию Вариантов Шейдеров

Во время игры в Редакторе Unity отслеживает, какие шейдеры и варианты в настоящее время используются в вашей сцене, и позволяет экспортировать это в коллекцию. Для этого перейдите в Настройки Проекта > Графика. Внизу вы заметите раздел Загрузка Шейдеров, показывающий, сколько шейдеров в настоящее время отслеживаются как активные.

Убедитесь, что вы нажали Очистить заранее, чтобы получить более точный образец, затем войдите в режим Игры и взаимодействуйте с вашей сценой, убедившись, что вы встретили все элементы игры, которые требуют конкретных шейдеров. Это увеличит отслеживаемые счетчики. Затем нажмите кнопку "Сохранить в актив" для сохранения всех этих в актив коллекции.

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

Кнопка Сохранить в актив

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

Добавление шейдера в Коллекцию Вариантов Шейдеров

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

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


Удаление вариантов шейдеров с помощью скриптов

Когда шейдер собирается в вашу сборку игры, Unity отправит обратный вызов. Это происходит как в сборках Player, так и в сборках Asset Bundles. Мы можем удобно слушать их, используя IPreprocessShaders.OnProcessShader и IPreprocessComputeShaders.OnProcessComputeShader (для вычислительных шейдеров), и добавлять пользовательскую логику для удаления ненужных вариантов. Таким образом, мы можем значительно сократить время сборки, размер сборки и общее количество вариантов, которые попадают в вашу сборку.

Для этого создайте скрипт, который реализует интерфейс IPreprocessShaders, затем напишите свою логику удаления в OnProcessShader. Например, вот скрипт, который удалит все варианты, содержащие ключевое слово шейдера DEBUG в сборках релиза:

public class StripDebugVariantsPreprocessor : IPreprocessShaders
{
   public int callbackOrder => 0;

   ShaderKeyword keywordToStrip;

   public StripDebugVariantsPreprocessor()
   {
      keywordToStrip = new ShaderKeyword("DEBUG");
   }


   public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
   {
      if (EditorUserBuildSettings.development)
      {
         return;
      }

      for (int i = data.Count - 1; i >= 0; i--)
      {
         if (data[i].shaderKeywordSet.IsEnabled(keywordToStrip))
         {
            data.RemoveAt(i);
         }
      }
   }
}

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

Посетите обсуждение Graphics-Shaders forum, чтобы узнать больше.

Больше ресурсов

Для получения дополнительной информации обратитесь к следующим разделам в руководстве Unity: