Приветствую.
Задался вопросом динамической генерации кода шейдера, но пока не знаю как к этому всему подступиться. Хочется в результате получить что-то типа узлового редактора материалов, т.е. собирать программу из неких заготовок: модели освещения, текстурирование, смешивание и т.п.
Кто уже занимался подобным или просто имеет представление как это сделать? Как хранить сниппеты и объединять их в конечный код? В гугле ничего не нашел, поэтому надеюсь на вашу помощь. API и ЯП не имеют значения, мне интересен сам подход.
CGAdept
> В гугле ничего не нашел
погугли Micro Shader (типа http://www.shawnhargreaves.com/hlsl_fragments/hlsl_fragments.html)
Возможно оно
CGAdept
> и объединять их в конечный код?
строки складывать. Вообще большей частью там будет работа со строками (вставка, замена, поиск)
Сначала определись какие функции должны быть доступны чрез подобное редактирование. У меня получилось около 17 функций с разной реализацией.
Делал подобную вещь но для OpenCL. По сути получается тупо работа со строками и все. Если интересно - https://github.com/Kvalme/libclnoise
war_zes, спасибо за отправную точку. Messenger, спасибо за код, но я подозреваю, что там не тупо со строками работать придется.
Вот, например, возьмем минимальный пиксельный шейдер, в котором финальный цвет задается константой:
gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0);
Допустим, я хочу передать в код параметр, определяющий финальный цвет.
gl_FragColor = myConst;
Ну и кроме того добавлю ключевые слова const vec4 myConst=... - это дело техники.
Теперь хочу регулировать интенсивность цвета из внешней программы, получится вот такое:
gl_FragColor = myFactor * myConst;
Собственно пока одно до меня не дойдет - в какой структуре данных хранить эту информацию. Пока предполагаю ориентированный граф.
Бунтарчик, мне бы пока с алгоритмом разобраться. Или набор предполагаемых функций очень важен даже на данном этапе? Я просто предполагаю держать в узле сниппет кода и подставляемые значения - какой угодно сложности, например, для Ламберта "saturate(dot(%0, %1))", где %0 - это нормаль, а %1 - вектор освещения. И в процессе сборки кода эти значения заменятся на то, что я укажу.
По простому - каждый узел хранит что-то типа:
res_type result_N = func(param1,param2);
Например имеем:
add:
vec4 result_N = param1 + param2;
mul:
vec4 result_N = param1 * param2;
result:
gl_FragColor = param1;
Затем когда генерируем, то выстраиваем по порядку строки заменяя шаблонные имена:
vec4 result_1 = 1.0+2.0; // Раскрытый add(1.0,2.0) vec4 result_2 = result_1*1.5; // Раскрытый mul add(result_1 ,1.5) gl_FragColor = result_2; // Раскрытый result(result_2)
Где числа 1.0,2.0,1.5 - для примера - можно из юниформов/аттрибутов достать.
Как-то так.
Che@ter
Спасибо, кое-что уже начало доходить) Чуть позже отпишусь о том, что получилось или к чему пришел.
По поводу хранения - я специально для использования этой либы писал гуй в котором строилась схема типа ((Perlin, Billow) ->Min->Gradient->PlaneMap) это в виде xml передается в генератор и уже создается сам код шейдера.
Для генерации кода разные модули вынесены в отдельные функции (см src/modules) где описаны их входы и выходы по которым и определяется можно ли построить такую схему и откуда брать параметры. Поэтому появилось в коде чтото вида float frequency = floatAtt[0];
Понятно что для других шейдеров это не проканает, но по сути просто надо будет не 2 массива использовать, а создать униформы или атрибуты
CGAdept
> мне бы пока с алгоритмом разобраться. Или набор предполагаемых функций очень
> важен даже на данном этапе?
Так вроде вся проблема только в том, как определить набор функций.
Потому что если у тебя OpenGL, то слинковать множество фрагментных шейдеров вместе не составляет труда, это входит в базовый функционал OpenGL. Соответственно берёшь свои функции, выносишь каждую из них в соответствющий шейдер, компилируешь все эти шейдеры и линкуешь вместе. У меня в движке так сделано.
Если интересно, вот стадии:
http://sourceforge.net/p/shengine/code/HEAD/tree/SH-Engine/Shaders/Stages/
Код линковки шейдеров:
http://sourceforge.net/p/shengine/code/HEAD/tree/SH-Engine/Includ… /shShader.cpp
Для удобства я сделал возможность использовать хедеры в шейдерах.
Например мне нравится как я сделал функцию эмиссии:
http://sourceforge.net/p/shengine/code/HEAD/tree/SH-Engine/Shader… Emission.glsl
Есть варианты:
"solid" - обычное свечение
"angular" - свечение с зависимостью от угла зрения (4-й параметр - степень)
"directed" - свечение с зависимостью от направления нормали (4-й параметр - степень)
"display" - эффект экрана (первые два параметра задают разрешение, третий - яркость)
"diffuse" - режим, при котором свечение выступает в роли эмбиента
В соответствии с заданным из материала режимом, подставляется нужный дефайн, который делает соответствующую ветку в шейдере.
Я сейчас пилю модульную систему шейдеров. Я придумал такую терминологию:
1) Интерфейс шейдера - это uniform'ы, которые он принимает
2) Формат вершин - это атрибуты вершинного шейдера
3) Тип модуля шейдера - это прототип функций для каждого шейдерного этапа, а также набор реализуемых интерфейсов. Также содержит в себе модуль своего типа по умолчанию
4) Модуль шейдера - реализация функций из соответствующего типа
5) Шаблон модуля - это объект, который может генерировать модуль шейдера каким-либо образом
а) Частный случай шаблона - паттерн модуля - просто строка с кодом, в которую потом подставляются значения параметров. Разные параметры приведут к созданию разных модулей
6) Шаблон шейдера - имеет фиксированный набор типов модулей. Представляет собой код main'а и может быть чего-то ещё, в коде указано место, куда должен подставляться полный код всех модулей генератора
7) Генератор шейдеров - указывает на шаблон шейдера и содержит в себе все модули. Вызовом ToShader() создаёт на основе всего этого готовый шейдер
Всё реализовано через строки, параметры хранятся в map<string, string>. Вручную в коде весь этот зоопарк настраивать неудобно, поэтому я собираюсь описывать всё это в файле. Сейчас делаю парсер. Синтаксис можно увидеть в примере ниже. Он Си-подобный, но строки там можно записывать не только в кавычках, но и между такими двойными скобками {[ и ]}, чтобы подсветка синтаксиса шейдерного кода в редакторах работала. Пока я ещё не всё дописал и ещё даже не проверял, тут могут быть некоторые недоработки и конфликты. Но в общем, идея должна быть понятна.
То есть надо один раз помучаться, написать такую штуку (на практике в итоге она будет раз в 5 подлиннее), а дальше собираешь любые комбинации модулей, какие хочешь, с любыми параметрами. Если гибкости не хватает, то либо расширить схему, добавив новые типы модулей, либо сделать отдельную схему.
Подпишусь
p.s у меня сейчас убер-шейдер
У меня получился почти свой язык с трансляцией в объектную модель и сборкой шейдеров из кусков. По сути, это такой компилятор убершейдера, только ifdef'ы оформляются не в коде шейдеров, а в отдельном коде модели материала.
Модель разделяется на несколько логических частей: интерфейс материала, интерфейс "сцены", биндинг инпутов (по сути, описание необходимых входных данных от вершин) и собственно, фрагменты кода из которых собираются шейдеры.
Интерфейсы материала и сцены это по сути наборы параметров различных типов (bool, float, color, vec, texture). Разница между интерфейсом материала и сцены в том, что по сути в интерфейсе материала описываются параметры, которые можно изменять для каждого отдельного объекта в сцене, а в интерфейсы сцены - глобальные для любого материала параметры (например - направление источника света, шадоумапы, включен ли АО и прочее). Есть не доделаный редактор под все это.
Приветствую.
Решил сначала поступить по примеру Che@ter. Просто писать свой псевдоязык пока не хочется. Вот на чем остановился. Есть такие типы блоков:
1. Вывод шейдера - корневой узел, для вершинного шейдера - gl_Position, для фрагментного - gl_FragColor.
2. Данные. Константа, текстурный сэмплер, либо юниформ, технически не важно - просто тип данных.
3. Операция. Унарная, бинарная или полноценная функция, в общем, что-то, что преобразовывает данные.
Каждый блок имеет:
1. Ссылку на другой блок, если в этом есть необходимость. Например, блок умножения имеет две ссылки на аргументы. Каждый из аргументов может быть как типом данных, так и операцией.
2. Функцию, которая вносит изменения в текст шейдерной программы согласно своему определенному поведению.
3. Идентификатор - для связки с другими блоками.
4. Другая служебная информация.
Также есть хранилище, где эти блоки просто хранятся списком.
В качестве примера приведу такой фрагмент кода:
vec3 finalCOlor = color * intesity;
gl_FragColor = vec4(finalColor, 1.0f);
Чтобы собрать этот шейдер в хранилище должны быть такие блоки:
1. Вывод шейдера - ссылается на блок №4.
2. Данные color.
3. Данные intensity.
4. Операция "Умножение" - ссылается на блоки №2 и №3.
Сборка кода начинается с вывода и проходит по всем ссылкам, составляя код будущего шейдера.
Однако, сдается мне, что иду я не по тому пути, ибо накладные расходы на создание такого шейдера выше, чем ручное написание кода. Как считаете?
P.S. Может разбавить текст пояснительными картинками?
CGAdept
> Однако, сдается мне, что иду я не по тому пути, ибо накладные расходы на
> создание такого шейдера выше, чем ручное написание кода. Как считаете?
Надеемся на компилятор. Возможно после создания такого шейдера нужно вручную пооптимизировать(поискать параметры с одиночным участием в других аргументах).
В целом шейдеры лучше писать вручную... это быстрее. Но при использовании такого графа можно будет вывести результат как в glsl, так и hlsl.
Пилю прототип, а там видно будет - стоит оно того или нет.
Тема в архиве.