Пример использования байдингов OpenGL для языка daScript
Попробовал разобраться с использованием байндингов библиотек к языку daScript. В качестве тестовой задачки решил портировать на daScript + OpenGL эту демку. Программа реализует алгоритм клиппинга модели несколькими плоскостями, с заполненнием отрезанных частей “крышками”, чтобы не было видно внутренней поверхности модели (скрин).
Алгоритм:
Отрисовать модель с шейдером отсечения (с “дырками”)
Заполнить буфер трафарета значениями так, чтобы пометить пиксели, которые нужно закрыть: — Отрисовать внутреннюю сторону модели, увеличивая значение в буфере трафарета — Отрисовать внешнюю сторону модели, уменьшая значение в буфере трафарета — Заполнять буфер трафарета в только в точках, в которых плоскость отсечения повернута к камере (для корректной работы нескольких плоскостей отсечения одновременно)
Отрисовать плоскости отсечения по полученной маске (получаются закрывающие “крышки”)
Каркас приложения
Для создания байндингов библиотек к daScript используется dasClangBind, с помощью которого сделаны обёртки для нескольких библиотек, включая OpenGL. Последний из примеров демонстрирует загрузку и отображение модели из obj-файла. Этот пример можно взять за основу. Для задания настроек отсекающих плоскостей можно взять байндинг к imgui.
Эти модули также тащат за собой glfw для создания окна и stbImage. Шаблон C++ кода для подключения модулей:
не забыли создать и очистить буфер трафарета (GL_STENCIL_BUFFER_BIT)
block в daScript — безымянная функция, которая захватывает переменные по ссылке (более быстрая, чем лямбда-функции, которые могут управлять способом захвата)
defer — макрос для добавления выражений в блок finally
Исходная демка использует библиотеки three.js и ColladaLoader.js для загрузки меша из dae файла, но можно конвертировать dae в obj, чтобы использовать код загрузки меша из примера daScript. Загрузка меша:
require opengl/opengl_gen
let mesh_file_name = "{get_das_root()}/house.obj"
var mesh <- load_obj_mesh(mesh_file_name) |> create_geometry_fragment
Интересная штука — DSL для работы с шейдерами (glsl_internal, набор макросов для того, чтобы писать шейдеры как обычные функции в daScript, а также работать с uniform переменными почти как с обычными переменными языка. Пример передачи uniform-ов в шейдер:
var [[uniform]] v_projection : float4x4 //объявление uniform переменной для шейдера
let aspect = display_h!=0 ? float(display_w)/float(display_h) : 1.
Помимо простой привязки функций библиотеки, сгенерированной с помощью dasClangBind, написаны также макросы для “daScript-ивизации” кода. Вместо императивного вызова функции glUniformXXX, программист декларирует намерение “эта переменная - uniform для шейдера” — аннотация uniform.
За счёт этого скриптовый язык становится не "условным бейсиком" для императивного вызова функций, а способом приблизить библиотеку к предметной области, в терминах которой мыслит и работает программист
Отрисовка:
glUseProgram(program)
vs_preview_bind_uniform(program)
fs_preview_bind_uniform(program)
draw_geometry_fragment(mesh)
Клиппинг плоскостями отсечения
Изменим пиксельный шейдер отрисовки объекта:
[fragment_program (version=400)]
def fs_preview
//если вершина отсекается плоскостями -- отбросить её
Сквозь отброшенные пиксели пока видны внутренние грани объекта.
Заполнение буфера трафарета
Исходная демка на three.js использует для описания состояния рендера концепцию материалов этой библиотеки, но несложно сопоставить свойства материлов с параметрами OpenGL
Как отмечено в описании алгоритма референсной демки, такой подход нормально работает с одной плоскостью отсечения, но с нескольими плоскостями даёт неверный результат (повёрнутые “от камеры” плоскости отсечения также вносят вклад в маску и портят результат в буфере трафарета — выводят лишние “дырки” или “крышки” в таких местах:
Коррекция буфера трафарета для граней, повернутых от камеры
На этом этапе обнаруживаются отличия между демкой-референсом и примером из daScript. Позиция камеры в референсной демке попадает в шейдера “автоматически”, эта переменная устанавливается библиотекой three.js. Для примера на daScript нужно передать её вручную и учесть то, что системы координат в демках различаются. Поворот в примере daScript задаётся через матрицу v_model, так что для трансформации камеры в систему координат модели и плоскостей отсечения нужно также “довернуть” её, умножив на матрицу модели.
//позиция камеры в пространстве координат модели
var [[uniform]] f_camera_position_rotated : float3
let camPos = v_model * float4(camera_position, 1.0)
f_camera_position_rotated = camPos.xyz
Можно обратить внимание на идентичный синтаксис умножения вектора на матрицу в коде вершинного шейдера для трансфорфмации вершин меша, и обычном скрипте на daScript.
Теперь наконец отсечения смотрятся корректно под любым углом.
daScript хорош, чтобы поиграться с демками графических эффектов :)
подход авторов к написанию байндингов — автоматическая обёртка на c/c++-функциями + “daScript-тификация” кода — создание макросов, упрощающих работу с библиотекой
вообще, демку стоило бы ещё перевести на режим live-изменений, тянет на отдельный туториал
Ну и более глобальный вывод про совокупность всех фич языка — если большая часть кода на языке делает то, что сложно или долго делать на других языках, с какого-то момента разработки сама программа может получить какие-то свойства, которых нет у программ на других языках (потому что их было слишком долго или трудно реализовывать).