${habrauser}, Привет!

При разработке игрового фреймворка Oriol Engine (которая, к слову, до сих пор ведётся) мы столкнулись с проблемой написания шейдеров для Cross-API рендеринга. В RHI-слой данного фреймворка было запланировано добавить поддержку таких графических API, как DX11/DX12, OpenGL и Vulkan.

И вот тут возникает вопрос: как же писать шейдеры на одном языке и обеспечить их поддержку на других графических API?

Для начала стоит определиться: на каком языке изначально мы планируем писать шейдеры? Из‑за приоритетов разработки игр в дальнейшем под продукцию Microsoft, в нашем случае логичнее переводить HLSL (версии 4 и 5) в GLSL, а не наоборот, ибо нет полного контроля над HLSL‑исходником.

Итак, уже начинает вырисовываться какая-то общая концепция: сначала конвертируем HLSL-исходник в GLSL, компилируем эти два исходника и помещаем в один бинарный файл под тегами [HLSL] и [GLSL] для дальнейшего удачного прочтения бинарника.

Да, в конечном итоге оно так и работает.

Схема работы компонента ShaderPack
Схема работы компонента ShaderPack

Почему же мы даже не стали смотреть в сторону SPIR-V? Потому что нам нужно было что-то компактное и самостоятельное, что можно добавить одним компонентом в фреймворк и дальше не париться.

Составление самого бинарного файла .shader особого интереса из себя не представляет, потому что это файл с довольно простой структурой: под тегами [HLSL] и [GLSL] хранится соответствующий байт-код того или иного исходника.

Структура .shader файла
Структура .shader файла

Но вот сама конвертация HLSL To GLSL (HTG) требует должного внимания. Сперва разберём этот компилятор на этапы: Frontend, Middle-end, Backend, и подробно рассмотрим каждый.

Этапы конвертации HLSL To GLSL (HTG)
Этапы конвертации HLSL To GLSL (HTG)

Frontend

Фронтовая часть HTG обрабатывает исходный HLSL-код в такой последовательности (мало чем отличается от frontend любого другого компилятора).

Препроцессинг

По официальному справочнику от Microsoft, препроцессор HLSL должен распознавать 12 директив: #define, #elif, #else, #endif и другие.

Не буду подробно про них здесь рассказывать. HTG в данном плане работает в точности по справочнику, поэтому welcome to the Microsoft Learn.

Парсинг

Здесь тоже всё достаточно стандартно. Сначала проводим лексический и синтаксический анализы. На этих этапах:

  • выявляются лексические и синтаксические ошибки;

  • проверяется соответствие HLSL‑спецификации;

  • извлекаются семантики (например, POSITIONTEXCOORD).

Middle-end

Данный этап является переходным в конвертации HLSL в GLSL. Здесь происходит трансляция в промежуточное низкоуровневое IR-представление на основе AST-дерева, близкое к GLSL. Сперва HLSL-семантики переводятся в GLSL-директивы (например, layout(location = 0)). Затем текстуры и сэмплеры разделяются, структуры и типы данных адаптируются под GLSL-синтаксис.

Backend

В конечном этапе IR транслируется в GLSL-код: формируются объявления переменных и функций, вставляются необходимые препроцессорные директивы, обрабатываются особенности целевой платформы (например, версии GLSL). К выходному коду применяются форматирование (отступы, переносы) и манглинг имён там, где это требуется.

Про последнее давайте поподробнее:

Для разных категорий идентификаторов применяются разные схемы генерации префикса или суффикса:

  • Глобальные переменные: g_ + хеш или уникальный индекс (например, g_var123).

  • Локальные переменные: l_ + номер блока + индекс (например, l_blk2_var4).

  • Параметры функций: p_ + имя функции + индекс (например, p_main_arg0).

  • Поля структур: s_ + имя структуры + имя поля (например, s_VertexInput_pos).

HLSL‑семантики преобразуются в GLSL‑атрибуты, а имя переменной может быть манглено для отражения семантики:

// HLSL
float4 pos : POSITION;
// GLSL
layout(location = 0) in vec4 s_VertexInput_pos;

Если два идентификатора после манглинга совпадают, к ним добавляется уникальный суффикс (например, _1_2).

Конец

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

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

Спасибо за прочтение :-)

Комментарии (5)


  1. Serpentine
    05.11.2025 17:15

    Александр, мое почтение! Даже если не выстрелит или забьете и приступите к другому проекту — все равно это хороший опыт.

    Хотелось бы побольше мяса и подробностей в статье, а также прочее: предыстория разработки фреймворка (курсовая/дипломная работа или другое) или с чего начинали, работали один или с командой, что читали, чем вдохновлялись, на какой он стадии (картинку выводит или нет), к чему хотите прийти и т.д.


    1. alex_02 Автор
      05.11.2025 17:15

      Воу! Спасибо) Вообще, 2 года работал с командой над игровым движком Case Engine (информацию можно найти в интернете), но то ли из-за нехватки опыта тогда, то ли из-за чего-то другого, проект не удалось довести до релиза. А этим летом решил начать разработку игрового фреймворка, ибо страсть делать игры на своём двигле не пропадала)

      Ну, делать движок ради движка это идея, конечно, сомнительная, потому что такую вещь всегда лучше писать под конкретную игру. Поэтому начал с дизайн документа самого игрового проекта Oriol и т.д.

      Что касается самого Oriol Engine, то фреймворк очень обширный, его части можно рассматривать на протяжении огромного количества статей. На данный момент полностью готов только его вспомогательный модуль Aid и пассивно-агрессивно разрабатывается графический Gfx модуль)


      1. Serpentine
        05.11.2025 17:15

        Про Case Engine тут слышал, две статьи их видел. Очень жаль, что не срослось с разработкой.

        Я уверен — ребята клевые, но вот на Хабре статьи не взлетели, т.к. не о том писали — от разработчиков движков (даже начинающих) ждешь какой-нибудь жести и офигительных историй, а не туториалы по VS 2022 и избитые учебные статьи в духе, как писать на плюсах.

        Под офигительными историями и жестью я понимаю: «Увидели на конференции такой-то доклад такой-то студии и решили запилить подобную фичу в наш движок. Сначала сделали так <описание во всех красках не очень гениального решения>, но не сложилось, потому что ... А затем мы пошли другим путем <описание во всех красках гениального решения> и всех победили. Вот вам бенчмарки и список литературы.».

        Не обязательно же отвлекаться от разработки, тратить время и разжевывать для самых маленьких, как пилить свой движок от А до Я и все его возможности на 100500 статей ради плюсиков на Хабре. Достаточно редко, но метко раз в полгода-год вот такое выдавать.

        В целом, желаю удачи.


  1. Jijiki
    05.11.2025 17:15

    есть утилита glslang, лучше не делайте сразу всё доделайте просто функционалы игры, тоесть движок через игру, может и не нужен будет кросс, кросс это уже движение во все стороны, да hlsl поинтереснее

    лучше эти силы потратить на модуль lua или аля ввод скрипта по-типо блюпринтов ( интерфейс, программирование логики доступной на скрипте ), смена поведений на горячую, всякие сценарии (или даже пулы сценариев может быть ), тоесть я предлагаю сконцентрироваться на нитках от кор до конечных идей в игру, попутно оборачивая это в движок тогда наверное уже

    по ниткам если идти там будет многое завершено, а кросс, во все стороны просто идёт поидее


    1. alex_02 Автор
      05.11.2025 17:15

      glslang это больше про разбор самого языка GLSL. В нашем же случае требуется разбирать только HLSL. А насчёт всего остального, я изначально планировал фреймворк как кроссплатформенный. Ибо такой момент как кроссплатформенность должен решаться в самом начале. Добавлять его где-то на середине работы это мрак)

      Насчёт lua или добавления других скриптовых языков, то это нарушило бы концепцию. Потому что цель сделать что-то похожее на SFML. Где весь код игры пишется на одних плюсах с помощью готовых компонентов фреймворка