Разработка на языке D – 13 лет спустя

13 лет назад я написал статью «Перспективы использования D в разработке игр» для журнала «FPS», в которой дал краткую характеристику основных особенностей языка и выделил главные его преимущества перед конкурентами. D изначально создавался как альтернатива C++ для разработки десктопных и серверных приложений – за прошедшие годы область применения языка не изменилась, хотя и добавились такие замечательные возможности, как поддержка ARM и Web Assembly. Но конъюнктура рынка и технологический статус кво сегодня уже несколько иные, поэтому я счел необходимым написать новую статью на ту же тему: насколько D актуален в геймдеве в 2023 году?

(далее…)

dlib 1.2 и другие обновления

Несколько обновлений за последние месяцы:

dlib 1.2.0 — добавлены функции гомотетии (homothetyMatrix, homothetyMatrix2D) в модуль dlib.math.transformation, функции конвертации радианов в обороты и обратно (radtorev, revtorad) в модуль dlib.math.utils. В dlib.image.render.shapes появилась функция отрисовки прямоугольника (drawRect) — автор реализации ReactiveAlkali. Добавлен файл CODE_OF_CONDUCT.md — краткий кодекс поведения для сообщества dlib:

Code of Conduct

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

1. Мы не приемлем нецензурные выражения, грубые и/или пренебрежительные сообщения, использование сексуальной, жестокой и иной оскорбительной лексики и изображений.

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

3. Этот проект держится в стороне от нетехнологических вопросов и тем. Мы приветствуем всех, независимо от возраста, гендерной идентичности, религии, этнической принадлежности, гражданства или культурного происхождения, и наше сообщество не придерживается какой-либо определенной идеологии. Сайты, репозитории, каналы связи и прочие ресурсы, связанные с dlib, не должны использоваться как площадка для пропаганды.

Также добавлена поддержка Doxygen для генерирования документации.

bindbc-wgpu 0.16.0 — синхронизация с wgpu-native 0.16.0.1. Кстати, также обновил демку wgpu-dlang, добавил поддержку мипмаппинга и перевел на dlib 1.2.0.

Dagon 0.15 пока, к сожалению, откладывается на неопределенный срок — не доходят руки сделать поддержку 1D-текстур, которой не хватает для того, чтобы реализовать загрузчик KTX. В марте добавил новые параметры постобработки для ручного управления эффектом Depth of Field (dofManual, dofNearStart, dofNearDistance, dofFarStart, dofFarDistance) — это нужно, если необходимо реализовать кастомную расфокусированность, труднодостижимую стандартными параметрами focalDepth, focalLength и fStop.

Hald CLUT

В Dagon появилась поддержка 3D-текстур, что позволило реализовать цветокоррекцию с использованием цветовых таблиц формата Hald CLUT. CLUT расшифровывается как color lookup table — таблица поиска цвета: в памяти хранится текстура, в которой стандартным цветам sRGB сопоставлены какие-то другие цвета — вместо оригинальных цветов пикселей на вывод идут значения, прочитанные из 3D-таблицы. Принцип примерно тот же, что использовался в индексированных цветовых режимах, только в данном случае таблица охватывает более широкий диапазон RGB. Чаще всего CLUT используется для имитации характерной «пленочной» цветовой гаммы на цифровых снимках, но ее возможности гораздо шире. В таблице цвета могут быть абсолютно любые — с математической точки зрения, она является функцией, которая переносит цвет из одного пространства в другое. Чем больше таблица, тем точнее ее охват.

Оргинал таблицы в формате Hald CLUT выглядит следующим образом (PNG можно скачать тут):

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

Преимущество Hald CLUT состоит в эффективном расположении значений — таблица размером 4096×4096 охватывает весь 24-битный диапазон sRGB (16777216 цветов) и при этом отлично сжимается в PNG. Для хранения таблицы важно использовать lossless-формат, так как сжатие с потерями вносит мелкие искажения в цвета, а в данном случае важно сохранить точность информации.

Еще одна немаловажная фича формата — прямая совместимость с 3D-текстурами OpenGL. Достаточно просто декодировать картинку в буфер RGB и создать из этого буфера текстуру функцией glTexImage3D — никаких промежуточных конвертаций не требуется. Эта текстура затем передается в шейдер постобработки, который выглядит совсем элементарно:

vec3 inputColor = texture(colorBuffer, texCoord).rgb;
vec3 outputColor = texture(lookupTable, inputColor).rgb;

В Dagon поддержка создания 3D-текстуры из двумерного буфера встроена в класс Texture. Нужно загрузить таблицу, создать новую текстуру и проинициализировать ее методом createFromImage3D. Результат передается в стандартный стек постобработки (game.postProcessingRenderer):

TextureAsset aCLUT;

override void beforeLoad()
{
    aCLUT = addTextureAsset("data/food.png");
}

override void afterLoad()
{
    Texture clut = New!Texture(assetManager);
    clut.createFromImage3D(aCLUT.image, 256);
    game.postProcessingRenderer.colorLookupTable = clut;
    game.postProcessingRenderer.lutEnabled = true;
}

Поддерживаются таблицы любых разрешений, но вы должны сами правильно вычислить размер 3D-текстуры, соответствующей вашей CLUT. Например, для таблицы 4096×4096 это будет 256x256x256, как в моем примере. Если в этот параметр передать неправильное значение, то будет построена некорректная текстура (в релизе обязательно добавлю валидацию).

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

Ссылки

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

Общее
https://timurgafarov.ru — портфолио с проектами и примерами дизайнерских работ (в будущем планирую сделать на этом домене сайт моего бизнеса);

Геймдев и разработка
https://github.com/gecko0307 — профиль на GitHub;
https://www.patreon.com/gecko0307 — страница на Patreon;
https://gamedev.timurgafarov.ru — блог о разработке игр на языке D;
https://timurgafarov.medium.com — блог о языке D на Medium;
https://www.youtube.com/channel/UCVaRZr3TpAWVP_kaqBfmoew — YouTube-канал;
https://xtreme3d.ru — портал, посвященный Xtreme3D, 3D-движку для Game Maker и Game Maker: Studio, а также, в целом, созданию 3D-игр на GM;
https://psxdev.xtreme3d.ru — страничка о разработке под первую PlayStation;
https://fps.xtreme3d.ru — журнал о цифровом творчестве;

Моделирование
https://www.artstation.com/gecko0307 — портфолио на ArtStation;
https://dobrofile.ru/gecko0307 — 3D-модели в продаже;
Также у меня до недавнего времени был профиль с моделями на CGTrader, но меня лишили статуса продавца в связи со всем известными событиями;

Искусство
https://art.timurgafarov.ru — основное художественное портфолио и краткая творческая биография;
https://www.deviantart.com/timurgafarov — профиль на DeviantArt;
https://www.behance.net/timurgafarov — профиль на Behance;
https://500px.com/p/gecko0307 — фототворчество;

Соцсети
https://twitter.com/gecko0307 — Twitter;
https://www.linkedin.com/in/timurgafarov — профиль на LinkedIn;
https://vk.com/tgafarov — страница ВКонтакте;

Разное
https://kadath.fandom.com — википроект по вселенной Страны Снов Лавкрафта;
https://timurgafarov.ru/pixeltavern — фанатский микроблог, посвященный игре Margonem.

Новая статья на Medium

Я написал статью «Almighty Alias» на Medium о моем любимом ключевом слове alias, основное назначение которого — определение псевдонимов к типам и объектам. Но на самом деле alias способен на большее, особенно в связке с шаблонами.

Новости по проектам

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

Из нового: практически доделал рефакторинг системы загрузки текстур в Dagon (ветка texture). Текстуры из DDS теперь загружаются напрямую, без создания промежуточных объектов SuperImage. Также 2D-текстуры и кубические карты объединены в один класс Texture, и работать с ними стало проще — например, загрузка карты окружения на стороне пользователя теперь выглядит одинаково как для кубической карты из DDS, так и для равнопромежуточной карты HDR. А еще появилась поддержка формата сжатия ASTC.

Обновил и выложил на GitHub свой старый программный растеризатор MiniGL. Попутно внес несколько улучшений — например, теперь конвейер поддерживает «шейдеры»: можно задать функции D, которые выполняются при обработке вершины и пикселя. Я не знаю, кому и для чего эта штука может пригодиться в 2022 году, но писать ее было весело, и код получился довольно наглядный и компактный — меньше 1000 строк, так что это как минимум хороший пример использования dlib.

И, наконец, около месяца у меня ушло на доработку одного интересного инструмента, не связанного с D, о котором я напишу подробнее в одном из следующих постов.

dlib 1.0

Спустя 10 лет разработки моя библиотека общего назначения для геймдева наконец-то стабилизировалась — итог этой работы отражает первый мажорный релиз проекта, dlib 1.0.0.

Из наиболее важных нововведений могу отметить ускорение загрузки изображений (от 2 до 10 раз в зависимости от формата и веса файла) — оптимизация заключается в том, чтобы декодировать из буфера в памяти, а не напрямую из файлового потока. Добавлена валидация при создании POSIX-потоков, улучшен модуль dlib.math.interpolation.hermite — добавлена функция вычисления производной для сплайна Эрмита. Исправлено несколько важных багов в математическом и геометрическом пакетах.

В связи с этим знаковым релизом, а также тем, что в этом году dlib как публичному OpenSource-проекту исполняется ровно 10 лет, хочу немного рассказать об истории этой разработки.

dlib фактически начался как порт разнородных исходников с C++ на D. В 2010-11 годах, когда я познакомился с D, у меня был свой небольшой игровой движок на C++ (Phantom3D), а также библиотека общего назначения Sparx, и я решил портировать их на D2 — язык в те годы как раз стабилизировался. Начал, естественно, с векторной алгебры — так появился dlib.math, старейший и наиболее законченный из пакетов dlib. Сам 3D-движок, конечно, претерпел немало трансформаций и, в итоге, от старого кода почти ничего не осталось — мой следующий движок DGL был написан почти с нуля, хотя и начинался как полный порт Phantom3D и сначала носил то же название.

Sparx я поначалу использовал в D в виде динамически слинкованной библиотеки, через Derelict. Но это было не очень удобно, поэтому следующим этапом стало переписывание Sparx на D, в результате чего родился dlib.image — Sparx использовался в основном как загрузчик ресурсов (изображений PNG, JPEG, JPEG2000, TGA, BMP, DDS, моделей OBJ и MD5). Сейчас, правда, старого кода из Sparx в библиотеке очень мало — но, например, отдельные участки кода dlib.image, некоторые математические и геометрические функции напрямую унаследованы оттуда.

Сама идея создать такую библиотеку появилась в результате осмысления чужих открытых проектов, в особенности Давида Анри (чудо, что его сайт до сих пор существует!). Изучая чей-нибудь код, я часто замечал, что многие функции и классы можно сделать обобщенными и независимыми, что позволяет переиспользовать их в самых разных проектах — наверное, около половины кода любой игры можно вынести в библиотеку общего назначения. Это, в первую очередь, код, который не зависит от графического API и логики движка — линейная алгебра, вычислительная геометрия, работа с файловой системой, многопоточностью, взаимодействие с ОС, сетью и т.д. Главный принцип — скрыть платформозависимый код под абстрактным интерфейсом, так, чтобы приложению не приходилось взаимодействовать с операционной системой напрямую. Это делает код приложения на удивление простым.

Но вернемся к проектам Давида Анри. Меня очень впечатлили его загрузчики PNG, TGA и BMP — они очень сильно повлияли на мои собственные реализации декодеров этих форматов. Декодер JPEG — отдельная история, я написал его полностью самостоятельно, не заглядывая в готовые реализации — только читая спеки и техническую литературу (фактически, из заимствований в нем только ДКТ-преобразователь — хардкорная fixed-point математика, написанная темными магами). Модули для работы с векторами и матрицами — тоже отчасти влияние Анри, а именно его библиотеки Mathlib. Мои реализации, конечно, за 10 лет стали намного лучше — в dlib все алгебраические объекты обобщенные, одно и то же описание используется для векторов и квадратных матриц всех стандартных размерностей (2, 3 и 4). Выше 4 я поддерживать не стал, так как в компьютерной графике они практически не используются. Также у меня есть оптимизированные функции инвертирования и декомпозиции матриц, свизлинг векторов, огромное множество функций-фабрик и различных операторов — практически все, что может понадобиться для любых вычислений, связанных с трехмерными моделями.

Точная дата юбилея dlib — 28 сентября, так как именно в этот день я создал репозиторий на Google Code (я тогда еще использовал SVN, а не Git). Самый интересный этап в жизни проекта начался в 2013, когда разработка была перенесена на GitHub. Появился модуль dlib.core, к проекту примкнули новые разработчики: Martin Сejp провел огромную работу по реализации потоков ввода-вывода и абстрактной файловой системы (dlib.core.stream, dlib.filesystem), Eugene Wissner написал сетевой пакет (dlib.network) и аллокаторы памяти (dlib.memory), Nick Papanastasiou написал модуль комбинаторики, Вадим Лопатин (к слову, автор знаменитой читалки Cool Reader) помог улучшить декодер PNG, Роман Чистоходов и Андрей Пенечко внесли множество исправляющих патчей. Отдельное спасибо Роману за улучшенную поддержку BMP и TGA, а также ценные советы.

В 2015 году я начал рефакторинг, связанный с поддержкой ручного управления памятью. Эта грандиозная работа была полностью завершена в 2019 году. Динамическая память — это отдельная большая история, далеко выходящая за рамки D, я мог бы написать целую книгу на эту тему. Если теория вычислений разработана математиками и инженерами очень глубоко и досконально, то «теории памяти», в общем-то, до сих пор нет. Все, что мы имеем научного в этой сфере — это теория типов и ее высшее достижение, система типов Хиндли-Милнера. К сожалению, нет какого-то общего, формального подхода к управлению динамической памятью — сколько языков, столько и практик. Полагаю, что необходимо выделять парадигмы памяти, подобно тому, как существуют парадигмы программирования в целом.

В dlib я реализовал парадигму единственного владельца (single ownership), которая очень хорошо ложится на базовое ООП. Каждый объект может «владеть» другими объектами. У любого объекта бывает только один владелец, либо нет владельца вовсе (у корневых объектов). Когда удаляется владелец, удаляются и все объекты, которыми он владеет. Такой подход позволяет полностью избавиться от сборщика мусора и сделать высвобождение памяти детерминированным. Объектами в этом контексте может быть все, что угодно — парадигма не зависит от логики приложения — но, естественно, речь идет о данных, которые создаются один раз и надолго (в предельном случае — о неизменяемых наборах данных, которые создаются при старте приложения и удаляются при завершении работы). Эта модель не очень хорошо работает с алгоритмами, где нужно создавать и уничтожать объекты многократно, в цикле — но на то есть аллокация в стеке. Именно подход к работе с динамической памятью я считаю одним из главных достижений dlib — эту фичу, я думаю, можно и нужно нести в мир, который, кажется, сдался без боя сборщикам мусора.

Впрочем, рефакторинг, связанный со сменой парадигмы памяти, не привел к тому, что dlib стала @nogc-библиотекой (то есть, формально, по контракту гарантирующей отсутствие вызовов сборщика). Этой целью пришлось пожертвовать для сохранения обратной совместимости API. Но у меня есть план вернуться к этой проблеме в следующей версии, dlib 2.0 — расскажу о своих идеях в одном из будущих постов.

В 2016 году появился пакет dlib.audio, который может служить бэкендом для звуковых движков, плееров, DAW и т.д. Пока реализованы только базовые функции, но пакет будет развиваться. Также появился пакет dlib.network, который содержит независимую от Phobos поддержку сокетов и функции, связанные с вебом. В 2021 году работа над основной функциональностью библиотеки была завершена.

dlib — это, наверное, самый грандиозный мой проект за всю жизнь. Начавшись как маленький набор модулей «на все случаи жизни» для экспериментов с OpenGL, библиотека превратилась в один из самых популярных пакетов в реестре Dub. Сейчас количество загрузок dlib составляет 800-1000 ежемесячно. На основе dlib пишутся игровые движки, инструменты анализа изображений, GUI-тулкиты, даже синтезаторы и библиотеки генетических алгоритмов. О такой широкой области применения я, честно говоря, изначально даже не думал! К сожалению, D все еще остается довольно нишевым языком, и востребованность библиотек на нем ограничена востребованностью самого языка — но я счастлив уже тем, что сделал для сообщества что-то полезное. Накопление алгоритмов, достижений математической и инженерной мысли в одном месте, в удобочитаемом виде — я считаю, дело нужное. И, в конечном счете, это был интересный путь.

Как ускорить загрузку изображений

Совет пользователям dlib. Не декодируйте изображение напрямую из файлового потока, это слишком медленно. Вместо этого рекомендую загрузить файл в память целиком, создать поток массива (ArrayStream) и уже его передавать в функцию-декодер:

InputStream input = openForInput("image.jpg");
ubyte[] data = New!(ubyte[])(input.size);
input.fillArray(data);
ArrayStream arrStrm = New!ArrayStream(data);
SuperImage img = loadJPEG(arrStrm);
Delete(arrStrm);
Delete(data);
input.close();

Для JPEG, например, это дает ускорение в 5-10 раз, в зависимости от размера картинки.

Новые статьи

Я написал две статьи на Medium (на английском):

Game UI with Nuklear — о тулкитах немедленного режима и о том, как использовать Nuklear в приложениях на основе Dagon. Я задумал цикл статей на эту тему, так как Nuklear поддерживает очень много всякого интересного, и на нем можно делать достаточно сложные вещи.

Metaprogramming with Alias Sequences — вводная статья о последовательностях псевдонимов. Это довольно мощный инструмент метапрограммирования в D, хотя и редко используемый.

DagoBan

DagoBan возвращается! Игра, изначально написанная Mateusz Muszyński в целях демонстрации возможностей тулкита Nuklear в Dagon, недавно была портирована мной на актуальную версию движка. Сборку для Windows можно скачать здесь.

Напомню, DagoBan — это мини-клон Sokoban на D со встроенным редактором уровней.