Пример работы с SFML в dasBox — рендер полупрозрачных объектов в рендер-таргет. Тривиальная задача для графического программиста, но почему-то многие разработчики игр не знают, как это корректно сделать.
Изначальная задумка поста — собрать небольшой пример, показывающий алгоритм рендеринга полупрозрачных объектов в текстуру, с последующим корректным отображением этой текстуры, как если бы объекты рендерились на экран напрямую, без использования этой текстуры. Но попутно оказалось, что можно запилить фреймворк для таких демок, с возможноостью в live-режиме поиграться с параметрами и посмотреть результат.
В отличие от GLFW, SFML представляет простые объекты-обёртки над функциями OpenGL для работы с текстурами, шейдерами, рендер-таргетами, и прочими примитивами библиотеки рендера, в то же время представляя для кастомизации практически все параметры рендера (во всяком случае необходимые для задуманного примера), для того, чтобы можно было писать небольшие примеры алгоритмов рендера на OpenGL.
Для начала, можно “завести” простейший пример из комплекта привязки SFML к daScript.
Для его сборки выкачиваем сабмодули daScript:
Затем включаем сборку этого модуля в CmakeLists.txt и добавляем линковку статической библиотеки с модулем libDasSFML и самих библиотек SFML:
и перегененируем проект
Затем в хост-приложении на C++:
run_script вызывает метод main из примера, который создаёт SFML-окно и запускает цикл обработки сообщений в нём.
В качестве фреймворка для создания окна используется имеющийся в SFML класс Window. Однако проще воспользоваться другим фреймворком, в который уже внедрены возможности daScript-а по перезагрузке кода.
dasbox — примитивный движок для 2d-игр, который Gaijin-ы использовали для проведения конкурса Gaijin Jam (игра-победитель). Движок имеет простую апишку, на которой можно потренироваться использованию языка на уровне “как будто типизированная lua”, а также настроен для подключения отладчика языка, веб-инспектора для отображения состояния ecs-модуля и горячей перезагрузке кода.
Но для меня интереснее другое — dasbox использует в качестве бекэнда SFML. И хотя это осознанно скрыто от пользователей для того, чтобы можно было сменить бекэнд, можно немного “поломать инкапсуляцию”, для того, чтобы воспользоваться в нём модулем dasSFML, и получить доступ к более богатому API, получив от самого dasbox-а фичи и инструменты по работе с кодом.
dasbox_sfml — порт дасбокса с включенным модулем dasSFML.
Базовый пример на нём выглядит так:
dasBox_sfml подхватывает изменения в файле с кодом, а также отображает в своей консоли ошибки в этом файле.
Идиомы daScript в примере:
using() - создание переменной временного типа, который существует только в пределах блока using
interface - приведение к базовому типу
Генератор привязок на C++ позволяет передать отношение наследования двух C++ типов в daScript так:
daScript заставляет думать о типах и их времени жизни немного больше, чем C++, но и позволяет выразить больше таких утверждений, которые сможет проверить компилятор, чтобы уберечься от ошибок.
Полупрозрачные круги, отрендеренные в текстуру, выглядят темнее, чем отрисованные на экране напрямую. С реальными текстурами (или отрендеренными в текстуру шрифтами), проблема обычно выглядит как темная рамка на краях объекта (там где края ради сглаживания плавно “уходят” в прозрачность).
Из-за чего это возникает?
Цвет пиксела отрисованного объекта смешивается в пикселем, уже находящимся в буфере цвете по настраиваемым формулам. При прямой отрисовке порядок рендера получается таким:
Тогда как в случае рендера в текстуру происходит:
Видно, что в итоговом цвете на экрана присутствует влияние цвета render-target текстуры — прозрачного по альфа каналу, но в итоговой формуле кроме альфа канала влияение оказывают также и RGB каналы цвета (чёрного или любого другого — неважно, но избавиться от влияния этого фонового цвета без изменения формулы смешивания невозможно, “невидимый” цвет начинает быть видимым).
OpenGL (и SFML над ней) дают возможность переключать формулы режимов смешивания. Важно отойти от представления “определенная формула магическим образом включает полупрозрачность” к тому, что графический API просто даёт возможность изменить уравнение смешивание, а вывод конкретных формул ложится на плечи программиста.
Стандартное уравнение смешивания для полупрозрачности смешивает цвета в пропорции:
При рендере в текстуру по умолчанию используется другая формула, которая разделяет рассчёты для RGB-каналов цвета и для A-канала.
Эта формула годится только для рендера полностью непрозрачных объектов. Если какие-то части объекта прозрачные, необходимо использовать другую пару формул (для рендера в текстуру + для рендера полученной текстуры на экране). Для рендера в текстуру при этом можно даже получать “неправильные” цвета, которые можно потом смешать с изображением на экране так, чтобы смешанный итоговый цвет стал корректным.
Одно из возможных решений — смешать цвет полученной render target текстуры в режиме Source-Over (термин из статьи выше, из списка операторов смешивания Портера-Даффа):
Код смешивания в режиме Source-Over, правильный результат:
Если посмотреть на коэффициенты смешивания в “неправильном” блендинге, и в Sourse-Over, можно прийти к более правильному интуитивному пониманию лишнего влияния цвета в неправильной версии. В неё не “вмешан” лишний чёрный цвет, а добавлено лишнее умножение цвета на альфа канал, которое “гасит” этот цвет больше, чем необходимо при корректном смешивании.
Можно пойти другим путём — убрать умножение цвета на альфу при смешивании совсем, но производить его 1 раз в шейдере — тогда не нужно переключать режим прозрачности, но нужно переключать шейдер, с которым отрисован объект. Преимуществом такого способами может быть то, что исходное изображение часто можно домножить на альфа-канал еще до запуска приложения (в демо-примере изображение генерится динамически, поэтому умножение сделано в шейдере).
Универсальный режим смешивания для premultiply изображений:
Заметки по daScript:
Вывод объекта с изменённым режимом блендинга и шейдером в SFML инкапуслирован в объекте RenderStates
Вместо создания временных типов в этом примере используются “обычные” для daScript указатели:
Такие указатели хранят объект в куче и владеют им, по семантике близко к std::unique_ptr. При желании можно освободить объект вручную, присвоив указателю nullptr.
ключевое слово implicit после имени типа в аргументах функции позволяет функции работать с обычными типами, так и с временными.