[Из песочницы] Отладка шейдеров на Java + Groovy
Подсветка синтаксиса шейдеров. Связь между шейдерами и внешними структурами данных. Юнит-тесты для шейдеров, дебаг, рефакторинг, статический анализ кода, и вообще полная поддержка 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 за помощь в написании статьи.