Текстурирование - один из самых эффективных шагов в рабочем процессе создания 3D-контента. Текстуры могут добавить детализации даже к очень простым сеткам. Однако они также могут быть абстрактными, и о них трудно рассуждать. Текстуры сопоставляются с поверхностью трехмерного изображения с использованием UV-координат способами, которые не всегда интуитивно понятны. Цветовые данные текстуры - числа, представляющие «красный», «синий», «зеленый» и «альфа-каналы» - могут использоваться для представления таких функций, как «металличность» или «шероховатость», а не видимого цвета. Отладка этих сложностей может оказаться сложной и трудоемкой, особенно потому, что для работы с текстурами обычно требуется вернуться к исходному программному обеспечению для создания (например, Photoshop или Substance Painter).
Мы хотели значительно упростить этот процесс. Несколько месяцев назад мы писали о том, что для нас значит простота. Мы создавали инструменты, чтобы сделать работу с Babylon более быстрой и простой. Инспектор текстур - один из таких инструментов.
В этом посте я покажу вам, как использовать инспектор текстур, а затем проведу вас через процесс его создания.
Использование инспектора текстур
Доступ к инспектору текстур можно получить из панели инспектора. Если вы находитесь на игровой площадке, вы можете нажать кнопку Инспектор на верхней панели, чтобы открыть инспектор.
Если вы работаете в своей среде, просто позвоните
scene.debugLayer.show();
В инспекторе просто выберите текстуру и нажмите кнопку «Редактировать» под предварительным просмотром текстуры.
Чтобы узнать больше о возможностях инспектора, посетите страницу документации.
Создание инспектора текстур для Интернета
Я присоединился к команде Babylon в качестве стажера в июне. Инспектором был мой проект стажировки, построенный в течение 12 недель. Попутно пришлось преодолеть несколько технических проблем. Инспектор интенсивно использует как 2D Canvas API, так и 3D-графику WebGL (на базе Babylon.js). Объединить эти два мира не всегда было легко.
Базовая структура такова: когда вы нажимаете редактировать на текстуре, она визуализируется с полным разрешением на холст, который затем служит базовыми пиксельными данными для редактора. Инструменты управляют этим холстом, а затем изменения загружаются обратно в графический процессор с использованием HTMLElementTexture
. Чтобы отразить изменения в сцене, мы просто меняем внутреннее свойство _texture
исходной текстуры на наше новое HTMLElementTexture
. Это означает, что сбросить ваши изменения так же просто, как вернуться к исходной внутренней текстуре (и снова отобразить ее на холсте).
Ниже приведены некоторые из наиболее сложных функций, которые я создал.
Картина на холсте
На самом деле у нас было только два требования к нашему инструменту рисования.
- Он должен был быть быстрым, чтобы пользователи могли точно контролировать, где они рисуют. Нет ничего хуже, чем наблюдать, как ваша кисть дрожит по холсту, когда вы пытаетесь ее перетащить из-за проблем с частотой кадров.
- Требовалось нарисовать точные значения RGBA на холсте, а не смешивать их с пикселями на холсте, используя альфа-значение (что является поведением Canvas по умолчанию).
Этот второй момент может потребовать небольшого пояснения. Допустим, вы хотите поместить красный кружок с альфа-каналом 50% на изображение дерева. Есть два способа интерпретировать альфу: либо как коэффициент смешения (в центре), либо как результирующее значение в канале A (справа). Мы хотели сделать последнее.
Это казалось довольно простыми условиями, но сложность заключалась в том, чтобы реализовать их одновременно.
Я начал с самой простой реализации, используя API штрихов холста. Когда вы начинали рисовать, я звонил moveTo
, чтобы подвести «перо» к позиции вашего курсора, и при каждом событии перемещения мыши я вызывал lineTo
, а затем stroke
. Это произвело красивую извилистую линию, и это было довольно быстро, но это дало странные результаты при рисовании с альфа-каналом ниже 100%. Я также не видел способа нарисовать точные значения RGBA с использованием этого подхода.
Моя первая мысль заключалась в том, что я могу заменить линии, нарисовав вместо этого серию кругов. Алгоритм был прост: вы начинаете с последней позиции курсора мыши и перемещаетесь в его текущее положение, размещая круги через равные промежутки времени, чтобы повторить мазки кисти. Вы не можете просто разместить один круг в кадре, потому что пользователи могут легко перемещать мышь по холсту дальше, чем радиус кисти в одном кадре.
Чтобы нарисовать альфа-значения напрямую, как обсуждалось ранее, мы фактически раскрашиваем каждый круг дважды. В первый раз мы используем режим наложения destination-out
, чтобы удалить пиксели на холсте. Во второй раз мы используем source-over
для размещения правильных значений цвета и альфа. Код выглядит примерно так:
Это выглядело очень хорошо! Прозрачность работала, как и предполагалось, и производительность была довольно хорошей. Была только одна проблема: эти уродливые артефакты сглаживания появлялись на краю каждого отдельного круга.
Возможно, отключение imageSmoothingEnabled
исправит это? Нет такой удачи. Насколько я могу судить, нет простого способа отключить AA при рисовании кругов с помощью Canvas.
В этот момент я начал расстраиваться. Я решил, что попробую напрямую манипулировать пикселями на холсте. Canvas предоставляет два метода getImageData
и putImageData
для работы с отдельными пикселями. Моя идея была проста: сканировать по всему холсту и вычислять, находится ли каждый пиксель в пределах определенного радиуса от линии между двумя положениями курсора. Если да, замените этот пиксель активным значением RGBA.
Это сработало, только с одной проблемой ... это было медленно. Мучительно медленно. Оказывается, ImageData
методы очень дороги, и вызывать их каждый кадр нельзя, особенно для больших текстур.
Таким образом, манипулирование пикселями в каждом кадре было невозможным. Но потом меня осенило; мы по-прежнему пытаемся равномерно распределить круги по холсту. И каждый из этих кругов идентичен. Что, если бы нам просто нужно было позвонить putImageData
один раз?
Вот как это работает в финальной версии:
- Когда пользователь начинает рисовать, мы создаем временный холст, ширина которого равна ширине мазка кисти.
- Используя
putImageData
, мы вручную генерируем точный круг, который нам нужен, с правильными значениями RGBA. - Каждый раз, когда пользователь перемещает курсор, мы раскрашиваем круг несколько раз, используя
drawImage
. Мы используем двухпроходный подход, обсужденный ранее, чтобы избежать смешения цветов.
Таким образом, нам нужно установить необработанные данные изображения только один раз, когда пользователь только начинает рисовать. Это не должно вызывать заметного заикания, за исключением, возможно, кисти очень большого размера.
Работа с каналами
Важной особенностью инспектора является возможность отключать каналы RGBA, что позволяет просматривать и управлять одним каналом изолированно. Например, вы можете использовать кисть, чтобы установить значения R пикселей в 0, не влияя при этом на значения GBA. Для просмотра это оказалось относительно тривиальным делом; мы использовали простой фрагментный шейдер для рендеринга текстуры в редакторе. Он использует условные операторы для объединения цветовых каналов в зависимости от того, что пользователь решил скрыть.
Монтаж оказался сложнее. Нам нужен был способ ограничить каналы, по которым инструменты могли писать. Мы не хотели реализовывать это в отдельных инструментах. Вот подход, который мы придумали:
- Редактор отправляет инструменту холст, содержащий копию текстуры.
- Инструмент может вносить любые изменения в этот холст, игнорируя ограничения каналов.
- Берем копию и оттягиваем ее на исходное полотно. Но если некоторые каналы заблокированы, мы вручную обрабатываем
ImageData
, чтобы выборочно копировать одни каналы, а не другие. К счастью,ImageData
хранится в виде массиваuint8
, где каждое целое число представляет один канал одного пикселя, поэтому разделить данные довольно просто.
Постобработка
У нас довольно рано возникла идея создать инструмент для регулировки контраста и экспозиции. В Babylon.js уже встроено множество мощных постпроцессов, в том числе ImageProcessing
шейдер, поддерживающий контраст и экспозицию. Вопрос был в том, как мы применили шейдер WebGL к 2D-холсту?
Хитрость заключалась в том, чтобы создать отдельный 3D-холст, содержащий одну неосвещенную плоскость, заполняющую все окно просмотра. 2D-холст используется в качестве исходной текстуры для этой плоскости. Когда мы визуализируем 3D-сцену, мы применяем желаемые ImageProcessing
эффекты. Затем мы загружаем отрисованные пиксели обратно в наш 2D-холст.
Это дает нам доступ ко всей мощи системы постобработки Babylon; реализация размытия или цветокоррекции в инструменте займет всего несколько минут.
Оптимизация производительности
Загрузка больших текстур в GPU обходится дорого. Под капотом HTMLElementTexture должен вызывать texImage2D
каждый раз, когда мы хотим вставить содержимое холста в нашу текстуру WebGL. Я пробовал использовать texSubImage2D
в качестве замены, но обнаружил, что он обеспечивает такую же или худшую производительность. В конечном итоге решение было столь же простым, как то, что обновления выполнялись каждые 32 мс. Оказывается, способность рисовать со скоростью 60 кадров в секунду намного важнее, чем просмотр обновления текстуры с максимально возможной частотой кадров.
Я надеюсь, что некоторые из этих решений будут полезны другим, борющимся с Canvas API. Я провел много часов, пробуя разные подходы, пытаясь выжать из них максимальную производительность. Несмотря на это, я уверен, что есть много вещей, которые можно было бы лучше оптимизировать. Если вы хотите внести свой вклад или хотите узнать, как все это работает, не стесняйтесь погрузиться в исходный код в репозитории Babylon GitHub!
Дарраг Берк - команда Babylon.js