Подсветка синтаксиса шейдеров. Связь между шейдерами и внешними структурами данных. Юнит-тесты для шейдеров, дебаг, рефакторинг, статический анализ кода, и вообще полная поддержка IDE. О том, как всё это получить, в чём подвох, и что прописать в мавене…
Создаем проект.
$ git clone https://github.com/kravchik/senjin
Копируем нативные библиотеки в корень проекта.
$ mvn nativedependencies:copy
Это позволит нам запускать файлы, в которых есть метод “main” по Ctrl+Shift+F10 (в IDEA) непосредственно из окна редактора, не беспокоясь о classpath.
Библиотека работает так: код шейдеров пишется на Groovy, затем транслируется в обычный glsl код. Шейдер в Groovy пишется как обычный код, который можно вызывать из Java. Шейдер использует те же поля и классы что и основная программа. Это позволяет IDE понимать как шейдер и остальной код связаны между собой, они для неё — обычные Groovy и Java классы. В результате имеем следующие удобства:
- поддержка IDE (рефакторинги, подсветка)
- статический анализ простых и не очень ошибок
- отладка шейдера
- юнит тесты для шейдеров
- связь между структурами буферов и шейдеров
Но даже если вы используете другие языки — вы всё равно можете держать шейдера в Groovy и Java. Вы не получите связки с остальным проектом, но юнит-тесты, дебаг, поддержка IDE будут доступны. Тогда в основном проекте просто будут использоваться авто-генерируемые файлы с glsl кодом.
Конкретный пример
Покажу основные моменты на примере specular shader (рендеринг “пластмассового материала”) — он достаточно простой, но в нём используются varying, uniform, attributes, текстуры, есть математика, в общем можно пощупать технологию.
Пиксельный шейдер
Это обычный Groovy-класс с методом main. Стандартные opengl-функции шейдер получает в наследство. Uniform переменные объявляются как поля шейдера. Код шейдера находится в функции main, но её объявление отличается от glsl — в явном виде указывается что попадает на вход шейдера (SpecularFi), и куда надо писать результат (StandardFrame). Так же пришлось отказаться от имен вида vec3, vec4, поскольку Groovy не удалось подружить с именами классов начинающихся с маленькой буквы.
public class SpecularF extends FragmentShaderParent<SpecularFi> {
public Sampler2D txt = new Sampler2D()
public float shininess = 10;
public Vec3f ambient = Vec3f(0.1, 0.1, 0.1);
public Vec3f lightDir
def void main(SpecularFi i, StandardFrame o) {
Vec3f color = texture(txt, i.uv).xyz;
Vec3f matSpec = Vec3f(0.6, 0.5, 0.3);
Vec3f lightColor = Vec3f(1, 1, 1);
Vec3f diffuse = color * max(0.0, dot(i.normal, lightDir)) * lightColor;
Vec3f r = normalize(reflect(normalize(i.csLightDir), normalize(i.csNormal)));
Vec3f specular = lightColor * matSpec * pow(max(0.0, dot(r, normalize(i.csEyeDir))), shininess);
o.gl_FragColor = Vec4f(ambient + diffuse + specular, 1);
}
}
Здесь уже можно увидеть достоинства подхода. Делаем небольшую ошибку в названии и IDE сразу сообщает об этом.
Смотрим что попадает на вход пиксельного шейдера (ctrl+space).
Запускаем юнит тест и смотрим в дебаге на вычисления.
Входные данные для пиксельного шейдера
SpecularFi (fragment input). Класс содержащий данные, являющиеся исходящими для вертексного шейдера и входящими для пиксельного.
public class SpecularFi extends BaseVSOutput {
public Vec3f normal;
public Vec3f csNormal;//cam space normal
public Vec3f csEyeDir;
public Vec2f uv;
public Vec3f csLightDir;//cam space light dir
}
Вертексный шейдер
Так же как и пиксельный шейдер — это Groovy-класс, с uniform переменными в полях и методом main с явным указанием классов входящих и исходящих данных.
class SpecularV extends VertexShaderParent<SpecularVi, SpecularFi> {
public Matrix3 normalMatrix;
public Matrix4 modelViewProjectionMatrix;
public Vec3f lightDir
void main(SpecularVi i, SpecularFi o) {
o.normal = i.normal
o.csNormal = normalMatrix * i.normal
o.gl_Position = modelViewProjectionMatrix * Vec4f(i.pos, 1)
o.csEyeDir = o.gl_Position.xyz
o.uv = i.uv
o.csLightDir = normalMatrix * lightDir
}
}
Входные данные для вертексного шейдера
SpecularVi (vertex input). Класс попадающий на вход вертексному шейдеру. Его же можно использовать для заполнения буфера данных, код которого без участия программиста договорится с кодом шейдера (прощайте glGetAttribLocation, glBindBuffer, glVertexAttribPointer и другие потроха).
public class SpecularVi {
public Vec3f normal;
public Vec3f pos;
public Vec2f uv;
}
Создание вертексного и пиксельного шейдера и объединение их в программу:
SpecularF fragmentShader = new SpecularF();
SpecularV vertexShader = new SpecularV();
GShader shaderProgram = new GShader(vertexShader, fragmentShader);
Как видно, их создание — это обычное инстанциирование классов. Шейдера оставляем в переменных чтобы позднее передать в них данные (степень блеска, направление света, и др.).
Далее создаётся буфер с данными. Здесь используется тот же класс, что попадал на вход вертексному шейдеру.
ReflectionVBO vbo1 = new ReflectionVBO();
vbo1.bindToShader(shaderProgram);
vbo1.setData(al(
new SpecularVi(v3(-5, -5, 0), v3(-1,-1, 1).normalized(), v2(0, 1)),
new SpecularVi(v3( 5, -5, 0), v3( 1,-1, 1).normalized(), v2(1, 1)),
new SpecularVi(v3( 5, 5, 0), v3( 1, 1, 1).normalized(), v2(1, 0)),
new SpecularVi(v3(-5, 5, 0), v3(-1, 1, 1).normalized(), v2(0, 0))));
vbo1.upload();
Заполнение входных данных для шейдеров. Передача параметров — просто выставление значений полей в Groovy-объектах шейдеров (которые предусмотрительно остались доступны в виде переменных).
fragmentShader.shininess = 100;
vertexShader.lightDir = new Vec3f(1, 1, 1).normalized();
//enable texture
texture.enable(0);
fragmentShader.txt.set(texture);
//give data to shader
shaderProgram.currentVBO = vbo1;
И, собственно, подключение шейдера и отрисовка.
shaderProgram.enable();
indices.draw();
Юнит-тест шейдера.
f.main(vso, frame);
assertEquals(1, frame.gl_FragColor.w, 0.000001);
assertEquals(1 + 0.1 + 0.6, frame.gl_FragColor.x, 0.0001);
assertEquals(1 + 0.1 + 0.5, frame.gl_FragColor.y, 0.0001);
assertEquals(1 + 0.1 + 0.3, frame.gl_FragColor.z, 0.0001);
Весь код примера находится тут.
Test.java //простой юнит-тест шейдера
RawSpecular.java //простейший мейник создающий картинку для хабра
SpecularF.groovy //пиксельный шейдер
SpecularV.groovy //вертексный шейдер
SpecularVi.java //класс, описывающий вертекс (specular Vertex shader Input)
SpecularFi.java //класс, описывающий данные идущие из вертексного в пиксельный (specular Fragment shader Input) шейдер
WatchSpecular.java //более сложный мейник с кнопками, мышью, и прочим, усложняющим понимание и улучшающим экспириенс
Библиотеку легко подключить через Maven:
<dependencies>
<dependency>
<groupId>yk</groupId>
<artifactId>senjin</artifactId>
<version>0.11</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>yk.senjin</id>
<url>https://github.com/kravchik/mvn-repo/raw/master</url>
</repository>
</repositories>
Ну и коротко о синтаксических отличиях:
- в теле шейдера используется Vec3f вместо vec3 (груви не удалось подружить с классом начинающимся с маленькой буквы)
- нет uniform — вместо них просто поля в шейдере
- нет varying, in, out — вместо них поля в классах, передаваемых в main
P.S. Проект я развиваю стихийно — то понадобится что-то, то просто интересно что-то сделать. Пока не успел сделать структуры и много чего другого. Если вам необходим какой-то функционал или направление развития (android? geometry shaders? kotlin?) — обращайтесь, обсудим!
Так же хочу выразить благодарность oshyshko и olexiy за помощь в написании статьи.
Комментарии (11)
fogone
27.10.2015 12:38Хороший концепт, я думал о подобной компиляции котлина в шейдер по аналогии с джаваскриптом, но конечно руки всё никак не доходят.
jorgen
27.10.2015 14:24Согласен, Kotlin был бы в трэнде :) Тоже смотрел на него, но не могу выделить время чтобы разобраться как из него AST выдрать. На stackoverflow не знают. Если умелец найдётся — соберём за пару дней.
vektory79
27.10.2015 14:36Да с AST там не сильно понятно. Создаётся впечатление, что это должно быть где-то на поверхности, но когда попытался разобраться, то быстро заблудился. Возможно опыт личных навыков в этой области не хватило. Так-то у них даже есть модуль под названием kotlin-compiler-embeddable. Но никакой информации по поводу него найти не удалось.
Скорее всего где-то там AST искать надо. Если будет время вечером — попробую сделать ещё один заход в ту сторону. По крайней мере теперь есть конкретная мотивация.fogone
28.10.2015 12:13Вот здесь видно как создается аст.
KotlinCoreEnvironment environmentForJS = KotlinCoreEnvironment.createForProduction(rootDisposable, configuration, EnvironmentConfigFiles.JS_CONFIG_FILES);
Сам аст для всех файлов получается несколькими строками ниже.
List<KtFile> sourcesFiles = environmentForJS.getSourceFiles();
vektory79
28.10.2015 14:24+1Да. Я вчера весь вечер разбирался что там да как. Больше всего потратил на то, чтобы понять как сформировать configuration правильно.
Просто изначально не догадался начать с kotlin-maven-plugin. Который простой как палка.
vektory79
ВАУ! Вы прям мои мысли прочитали. Сам давно думаю над подобным, но всё никак не соберусь с духом. А тут уже такой задел хороший.
Правда все мои эксперименты крутятся вокруг геометрических шейдеров. Так что их точно хотелось бы увидеть (или доделать :).
По поводу Kotlin — тоже интересно. Нужно будет на досуге глянуть что да как у Вас сделано. Сейчас как раз развлекаюсь с переписыванием некоторых хитрых алгоритмов на котлин. Очень результат нравится.
И ещё вопрос: а у вас шейдеры какой версии генерируются? Лично меня интересуют возможности 4.2 и выше. Но в тоже время я понимаю, что это только мой интерес. Большинство сейчас на Android затачивается и в результате ограничивает себя исключительно версиями около 3.3. Возможно имеет смысл предусмотреть какой-то способ указания версии шейдеров.
Так же подозреваю, что за уадром осталась довольно интересная история по разработке этой красоты с техническими подробностями :-). Думаю многим было бы интересно :-).
P.S. Огромное спасибо за статью. Буду напосмотреть.
jorgen
Рад что не только мне это интересно!
Сейчас версия шейдера — 130. Да, хочется поддерживать разные версии, и геометрический шейдер — хороший повод :)
Я абсолютно не представляю что можно рассказать об истории разработки. Может перечислите что было бы интересно услышать лично вам?
vektory79
Ну, например, бегло пробежавшись по исходникам, даже не понял толком, как это работает. Хотя толком не вникал. Было бы интересно получить некоторую развёрнутую информация как реализован функционал. Какие и как использовались технологии. Чтобы другим было легче разобраться и, возможно, попробовать что-либо добавить.
Ну как-то так. Извиняюсь, если слишком сумбурно. Просто тема очень взбудоражила, а работа тоже не ждёт…
jorgen
Люблю сумбурные ТЗ, они — декларативны :)
jorgen
Могу поотвечать на вопросы в чатике
vektory79
Большое спасибо. Как начну разбираться — обязательно обращусь. Спасибо!