Эффективный доступ к данным текстур

NICO LEYMAN Software Development Consultant
May 25, 2023|15 Мин
Эффективный доступ к данным текстур
Эта веб-страница была переведена с помощью машинного перевода для вашего удобства. Мы не можем гарантировать точность или надежность переведенного контента. Если у вас есть вопросы о точности переведенного контента, обращайтесь к официальной английской версии веб-страницы.

Узнайте о преимуществах и недостатках различных способов доступа к базовым данным пикселей текстуры в вашем проекте Unity.

Работа с пиксельными данными в Unity

Пиксельные данные описывают цвет отдельных пикселей в текстуре. Unity предоставляет методы, позволяющие считывать и записывать данные пикселей с помощью скриптов C#.

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

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

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

Копии пиксельных данных на CPU и GPU

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

Различия между центральным процессором и графическим процессором
Память

На большинстве аппаратных средств память, доступная центральному процессору, отличается от памяти графического процессора. Некоторые устройства имеют форму частично общей памяти, но в этом блоге мы будем исходить из классической конфигурации ПК, где центральный процессор имеет прямой доступ только к оперативной памяти, подключенной к материнской плате, а графический процессор использует собственную видеопамять (VRAM). Любые данные, передаваемые между этими различными средами, должны проходить через шину PCI, что медленнее, чем передача данных в пределах одного и того же типа памяти. Из-за этих затрат следует попытаться ограничить объем данных, передаваемых за один кадр.

Блок-схема, визуализирующая взаимосвязь между памятью ЦП и ГП, а также поперечное сечение API
Обработка

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

В некоторых случаях может быть предпочтительнее обрабатывать данные текстур на центральном процессоре, что обеспечивает большую гибкость в доступе к данным. Операции с пиксельными данными ЦП действуют только на копию данных ЦП, поэтому требуют читаемых текстур. Если вы хотите сэмплировать обновленные пиксельные данные в шейдере, вам необходимо сначала скопировать их из CPU в GPU, вызвав Apply. В зависимости от используемой текстуры и сложности операций может оказаться быстрее и проще придерживаться операций ЦП (например, при копировании нескольких 2D-текстур в ресурс Texture2DArray).

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

Чтобы определить оптимальное решение, ответьте на следующие вопросы:

  • Может ли графический процессор выполнять вычисления быстрее центрального процессора?
  • Какую нагрузку этот процесс оказывает на кэши текстур? (Например, выборка большого количества текстур высокого разрешения без использования MIP-текстур, скорее всего, замедлит работу графического процессора.)
  • Требуется ли для этого процесса случайная текстура записи или возможен вывод на цветное или глубинное устройство? (Запись в случайные пиксели текстуры требует частой очистки кэша, что замедляет процесс.)
  • Неужели мой проект уже уперся в узкие места в графическом процессоре? Даже если графический процессор способен выполнять процесс быстрее центрального процессора, может ли графический процессор позволить себе взять на себя больше работы, не превышая свой бюджет времени кадра?
  • Если и основной поток графического процессора, и основной поток центрального процессора близки к пределу времени кадра, то, возможно, медленную часть процесса можно будет выполнить рабочими потоками центрального процессора.
  • Какой объем данных необходимо загрузить на графический процессор или выгрузить с него для расчета или обработки результатов?
  • Может ли шейдер или задание C# упаковать данные в меньший формат, чтобы уменьшить требуемую полосу пропускания?
  • Можно ли уменьшить разрешение RenderTexture до версии с меньшим разрешением, которую можно загрузить?
  • Можно ли выполнять процесс по частям? (Если необходимо обработать большой объем данных одновременно, существует риск, что у графического процессора не хватит для этого памяти.)
  • Насколько быстро требуются результаты? Можно ли выполнять вычисления или передачу данных асинхронно и обрабатывать их позже? (Если в одном кадре выполняется слишком много работы, существует риск, что у графического процессора не будет достаточно времени для рендеринга фактической графики для каждого кадра.)
Делаем текстуру читаемой или нечитаемой

По умолчанию текстурные ресурсы, импортируемые в проект, не читаются, тогда как текстуры, созданные с помощью скрипта, читаются.

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

Чтобы проверить, доступен ли для чтения ресурс текстуры в вашем проекте, и внести изменения, используйте параметр «Разрешено чтение/запись» в настройках импорта текстурили API TextureImporter.isReadable .

Чтобы сделать текстуру нечитаемой, вызовите ее метод Apply с параметром makeNoLongerReadable, установленным в значение «true» (например, Texture2D.Apply или Cubemap.Apply). Нечитаемую текстуру невозможно сделать снова читаемой.

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

Примеры API доступа к текстурам на GitHub
Пример текстуры, генерируемой на ЦП в каждом кадре

Производительность существенно различается в зависимости от способа доступа к текстурным данным, особенно на ЦП (хотя при более низких разрешениях это проявляется в меньшей степени). Репозиторий примеров API доступа к текстурам Unity на GitHub содержит ряд примеров, демонстрирующих различия в производительности между различными API, которые позволяют получать доступ к данным текстур или манипулировать ими. Пользовательский интерфейс отображает только тайминги процессора основного потока. В некоторых случаях для максимизации производительности используются такие функции DOTS, как Burst и система заданий .

Вот примеры, включенные в репозиторий GitHub:

  • ПростойКопировать: Копирование всех пикселей из одной текстуры в другую
  • PlasmaTexture: Текстура плазмы обновляется на ЦП за кадр
  • TransferGPUTexture: Перенос (копирование в другой размер или формат) всех пикселей на GPU из текстуры в RenderTexture

Ниже приведены показатели производительности, взятые из примеров на GitHub. Эти цифры используются для обоснования приведенных ниже рекомендаций. Измерения проводились на плеере, построенном на системе с 8-ядерным процессором Xeon® W-2145 с тактовой частотой 3,7 ГГц и видеокартой RTX 2080.

Пример SimpleCopy

Это среднее время ЦП для SimpleCopy.UpdateTestCase с размером текстуры 2048.

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

Результаты

  • 1326 мс – foreach(mip) for(x в ширину) for(y в высоту) SetPixel(x, y, GetPixel(x, y, mip), mip)
  • 32,14 мс – foreach(mip) SetPixels(source.GetPixels(mip), mip)
  • 6,96 мс – foreach(mip) SetPixels32(source.GetPixels32(mip), mip)
  • 6,74 мс – LoadRawTextureData(source.GetRawTextureData())
  • 3,54 мс – Graphics.CopyTexture(readableSource, readableTarget)
  • 2,87 мс – foreach(mip) SetPixelData<байт>(mip, GetPixelData<байт>(mip))
  • 2,87 мс – LoadRawTextureData(source.GetRawTextureData<byte>())
  • 0.00 мс – Graphics.ConvertTexture(источник, цель)
  • 0.00 мс – Graphics.CopyTexture(nonReadableSource, target)
Пример PlasmaTexture

Это среднее время ЦП для PlasmaTexture.UpdateTestCase с размером текстуры 512.

Вы увидите, что SetPixels32 неожиданно медленнее, чем SetPixels. Это связано с необходимостью брать результат цвета на основе плавающей точки из расчета пикселей плазмы и преобразовывать его в байтовую структуру Color32. SetPixels32NoConversion пропускает это преобразование и просто присваивает выходному массиву Color32 значение по умолчанию, что обеспечивает лучшую производительность, чем SetPixels. Чтобы превзойти производительность SetPixels и базового преобразования цветов, выполняемого Unity, необходимо переработать сам метод расчета пикселей, чтобы напрямую выводить значение Color32. Простая реализация с использованием SetPixelData почти гарантированно даст лучшие результаты, чем осторожные подходы SetPixels и SetPixels32.

Результаты

  • 126,95 мс – SetPixel
  • 113,16 мс – SetPixels32
  • 88,96 мс – SetPixels
  • 86,30 мс – SetPixels32NoConversion
  • 16,91 мс – SetPixelDataBurst
  • 4,27 мс – SetPixelDataBurstParallel
Пример TransferGPUTexture

Это время работы графического процессора редактора для TransferGPUTexture.UpdateTestCase с размером текстуры 8196:

  • Блит – 1,584 мс
  • КопироватьТекстуру – 0,882 мс
Рекомендации API пиксельных данных

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

КопироватьТекстуру

CopyTexture — самый быстрый способ переноса данных GPU из одной текстуры в другую. Он не выполняет никаких преобразований форматов. Вы можете частично скопировать данные, указав исходное и целевое положение, а также ширину и высоту области. Если обе текстуры читабельны, операция копирования будет также выполнена над данными ЦП, что приближает общую стоимость этого метода к стоимости копирования только ЦП с использованием SetPixelData с результатом GetPixelData из исходной текстуры.

Блит

Blit — это быстрый и мощный метод передачи данных GPU в RenderTexture с использованием шейдера. На практике это должно настроить состояние API графического конвейера для рендеринга в целевую RenderTexture. По сравнению с CopyTexture, он имеет небольшие затраты на установку, не зависящие от разрешения. Шейдер Blit по умолчанию, используемый методом, принимает входную текстуру и отображает ее в целевой RenderTexture. Предоставляя пользовательский материал или шейдер, вы можете определять сложные процессы рендеринга «текстура-текстура».

GetPixelData и SetPixelData

GetPixelData и SetPixelData (вместе с GetRawTextureData) — самые быстрые методы, которые можно использовать, когда речь идет только о данных ЦП. Оба метода требуют предоставления типа структуры в качестве параметра шаблона, используемого для повторной интерпретации данных. Самим методам эта структура нужна только для получения правильного размера, поэтому вы можете просто использовать byte, если не хотите определять пользовательскую структуру для представления формата текстуры.

При доступе к отдельным пикселям хорошей идеей будет определить пользовательскую структуру с некоторыми служебными методами для простоты использования. Например, структура формата R5G5B5A1 может быть составлена из члена данных ushort и нескольких методов get/set для доступа к отдельным каналам как к байтам.

Неизвестный тип блока «codeBlock», укажите сериализатор для него в свойстве `serializers.types`

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

SetPixelData можно использовать для копирования полного уровня MIP-данных в целевую текстуру. GetPixelData вернет NativeArray, который фактически указывает на один уровень MIP внутренних данных текстур ЦП Unity. Это позволяет вам напрямую считывать/записывать эти данные без необходимости каких-либо операций копирования. Проблема в том, что возвращаемый GetPixelData NativeArray гарантированно будет действителен только до тех пор, пока пользовательский код, вызывающий GetPixelData, не вернет управление Unity, например, когда возвращается MonoBehaviour.Update. Вместо того чтобы сохранять результат GetPixelData между кадрами, вам придется получать правильный NativeArray из GetPixelData для каждого кадра, из которого вы хотите получить доступ к этим данным.

Применять

Метод Apply возвращает значение после загрузки данных CPU в GPU. Параметр makeNoLongerReadable следует по возможности установить на «true», чтобы освободить память данных ЦП после загрузки.

RequestIntoNativeArray и RequestIntoNativeSlice

Методы RequestIntoNativeArray и RequestIntoNativeSlice асинхронно загружают данные GPU из указанной текстуры в (часть) NativeArray, предоставленного пользователем.

Вызов методов вернет дескриптор запроса, который может указать, завершена ли загрузка запрошенных данных. Поддержка ограничена лишь несколькими форматами, поэтому используйте SystemInfo.IsFormatSupported с FormatUsage.ReadPixels для проверки поддержки форматов. Класс AsyncGPUReadback также имеет метод Request , который выделяет вам NativeArray. Если вам необходимо повторить эту операцию, вы получите лучшую производительность, если вместо этого выделите NativeArray и будете использовать его повторно.

Методы, которые следует использовать с осторожностью

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

Пиксельные аксессоры с базовыми преобразованиями

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

Быстрые средства доступа к данным с подвохом

GetRawTextureData и LoadRawTextureData — это методы, работающие только с Texture2D и работающие с массивами, содержащими необработанные пиксельные данные всех уровней MIP, один за другим. Макет идет от самого большого к самому маленькому mip-объекту, при этом каждый mip-объект представляет собой сумму значений пикселей «высота» и «ширина». Эти функции быстро предоставляют доступ к данным ЦП. У GetRawTextureData есть «подводный камень», когда нешаблонный вариант возвращает копию данных. Это немного медленнее и не позволяет напрямую манипулировать базовым буфером, управляемым Unity. GetPixelData не имеет этой особенности и может возвращать только NativeArray, указывающий на базовый буфер, который остается действительным до тех пор, пока пользовательский код не вернет управление Unity.

КонвертироватьТекстуру

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

Выделите временную RenderTexture, соответствующую целевой текстуре.

Выполните Blit из исходной текстуры во временную RenderTexture.

Скопируйте результат Blit из временной RenderTexture в целевую текстуру.

Ответьте на следующие вопросы, чтобы определить, подходит ли этот метод для вашего варианта использования:

  • Нужно ли мне выполнять это преобразование?
  • Могу ли я убедиться, что исходная текстура создана в нужном размере/формате для целевой платформы во время импорта?
  • Могу ли я изменить свои процессы так, чтобы они использовали те же форматы, что позволит использовать результат одного процесса напрямую в качестве входных данных для другого процесса?
  • Могу ли я вместо этого создать и использовать RenderTexture в качестве назначения? Это сократит процесс преобразования до одного Blit в целевой RenderTexture.
ReadPixels

Метод ReadPixels синхронно загружает данные GPU из активной RenderTexture (RenderTexture.active) в данные CPU Texture2D. Это позволяет сохранять или обрабатывать выходные данные операции рендеринга. Поддержка ограничена лишь несколькими форматами, поэтому используйте SystemInfo.IsFormatSupported с FormatUsage.ReadPixels для проверки поддержки форматов.

Обратная загрузка данных из графического процессора — медленный процесс. Прежде чем начать, ReadPixels должен дождаться, пока графический процессор завершит всю предыдущую работу. Лучше избегать этого метода, поскольку он не вернет данные, пока они не будут доступны, что снизит производительность. Удобство использования также вызывает беспокойство, поскольку данные GPU должны находиться в RenderTexture, который должен быть настроен как активный в данный момент. При использовании методов AsyncGPUReadback , рассмотренных ранее, повышается удобство использования и производительность.

Методы конвертации между форматами файлов изображений

Класс ImageConversion содержит методы для преобразования между Texture2D и несколькими форматами файлов изображений. LoadImage может загружать данные JPG, PNG или EXR (начиная с версии 2023.1) в Texture2D и выгружать их в графический процессор. Загруженные пиксельные данные можно сжимать «на лету» в зависимости от исходного формата Texture2D. Другие методы могут преобразовывать массив данных Texture2D или пикселей в массив данных JPG, PNG, TGA или EXR.

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

Основные выводы и более продвинутые ресурсы

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

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

Множество дополнительных рекомендаций можно найти в практическом центре Unity.

Вот краткое изложение ключевых моментов, которые следует запомнить:

  • При работе с текстурами первым шагом является оценка того, какие операции можно выполнить на графическом процессоре для достижения оптимальной производительности. Ключевыми факторами, которые следует учитывать, являются текущая нагрузка на ЦП/ГП и размер входных/выходных данных.
  • Использование низкоуровневых функций, таких как GetRawTextureData, для реализации определенного пути преобразования там, где это необходимо, может обеспечить повышенную производительность по сравнению с более удобными методами, которые выполняют (часто избыточные) копии и преобразования.
  • Более сложные операции, такие как большие обратные считывания и пиксельные вычисления, возможны на ЦП только при асинхронном или параллельном выполнении. Сочетание Burst и системы заданий позволяет C# выполнять определенные операции, которые в противном случае были бы эффективны только на графическом процессоре.
  • Профиль часто: В процессе разработки можно столкнуться со множеством подводных камней: от неожиданных и ненужных преобразований до остановок из-за ожидания другого процесса. Некоторые проблемы с производительностью начнут проявляться только по мере масштабирования игры и более интенсивного использования определенных частей вашего кода. Пример проекта демонстрирует, как, казалось бы, небольшое увеличение разрешения текстур может привести к возникновению проблем с производительностью определенных API.

Поделитесь с нами своими отзывами о текстурных данных на форумах «Скриптинг» или «Общая графика» . Обязательно следите за новыми техническими блогами от других разработчиков Unity в рамках текущего Серия«Технологии из окопов».