Пишем небольшой шейдер подсветки интерактивных объектов для игры Jarl (на модифицированном Bevy Engine) – разбор + код
Небольшая заметка о том как написать аутлайн-шейдер для рисования границ объектов на примере пиксель арт игры Jarl (Discord, YouTube, Twitter, Reddit). Материал предполагает наличие у читателя базового знания и интереса к графическим технологиям. Игра использует модифицированый движок Bevy и языки Rust + WGSL.
Сразу к сути – как это работает (очень условно):
1. Создаем отдельные рендер таргеты (промежуточные текстуры в которые будем рисовать эффект) для объектов которые нужно подсветить и для промежуточных результатов с нужными флагами для использования:
1) Флаг для рендер пайплайна RENDER_ATTACHMENT (текстуру можно использовать в стандартном render pipeline для отрисовки)
2) Флаг для чтения из компьют шейдере STORAGE_BINDING (текстуру можно читать в компьют шейдере для обработки):
2. Создаем 2D камеру которая будет видеть только те объекты которые нужно посвечивать и присваиваем ей отдельный слой RenderLayers – CAMERA_LAYER_OUTLINES. В Bevy пока что нельзя задать для одной камеры несколько таргетов стандартными средствами, поэтому придется делать такой костыль. Хотя в моем форке это было поправлено.
3. Иерархиям сущностей которые нужно подсветить добавляем два компонента: один для включения подсветки, второй для системы индескирования (чтобы быстро находить объекты которые близки к курсору). Тут просто работа с ECS. Код приводить не буду поскольку это тривиально.
4. В рантайме выбираем под-иерархии тех сущностей которые прошли тест на близость с курсором и отрисовываем их в нужный таргет добавляя его через RenderLayers (после чего нужно также не забыть его убрать):
5. Теперь самая нудная часть если у вашего движка нет шейдер графа – руками настроить сами все нужные пайплайны для WGPU, описать типы, входы, выходы и пр. Здесь нужно пройти все церемонии перед тем как начать писать шейдер который будет читать из текстуры объекты которые нужно подсветить и рисовать для них аутлайн.
5.1 Создаем Binding Group — коллекцию ресурсов (например, текстур и буферов), которая объединяется и привязывается к шейдеру. Привязываем ее к 1) текстуре из которой будем читать объекты 2) к промежуточной текстуре куда будем писать аутлайн:
5.2 Создаем Bind Group Layout — описание структур и типов ресурсов, которые будут связаны с шейдером. Здесь прописываем все типы и свойства используемых ресурсов в рамках конкретного шейдера.
5.3 Создаем сам компьют пайплайн (в больших движках это как правило делается в редакторе материалов) который объединяет шейдер, ресурсы и описание типов. Compute Pipeline это объект, определяющий, как шейдер будет выполняться на этапе вычислений (включая его конфигурацию, связанную раскладку ресурсов и точку входа):
5.4 Диспатчим шейдер для каждого кадра, здесь все тривиально:
5. Самое интересное – это сам шейдер который все это отрисовывает. Тут особо никакой магии, берем кернел 3x3 (размер на ваше усмотрение) и проходимся им по всей текстуре с объектами проверяя сколько пикселей имеют альфа-значение выше какого-то порога. Если количество таких пикселей больше нуля, но меньше скажем 90%, то значит у нас граница объекта. Здесь можно экспериментировать с разными параметрами и кернелами, но для начала и так сойдет.
Что можно улучшить?
- Использовать GPU object picking вместо индекса на CPU. В этом методе вместо цвета, в первую текстуру пишутся ID объектов после чего читается ID пикселя на который указывает курсор мыши. Этот метод может работать быстрее, но нам он не подходит. Игра должна подсвечивать объекты которые находятся близко к курсору, а пиксельная точность будет только мешать.
- Передавать цвет которым нужно рисовать контур, этот код был пропущен для упрощения материала.
- Думаю можно было бы обойтись пиксельным шейдером и немного укорить отрисовку, но это как нибудь потом.
- Поскольку это пиксель арт игра, то эффект можно и запеч.