[Из песочницы] Отладка шейдеров на Java + Groovy

602ce800a93f4c2bb090f0b42a12259a.png

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

Конкретный пример


e5ceed34c0ce4451ada8d7cba6009e67.png
Покажу основные моменты на примере 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 сразу сообщает об этом.

12eb5e2a187d460cb4873625b4169c6e.png

Смотрим что попадает на вход пиксельного шейдера (ctrl+space).

81d9859aece145b082134bb921305c9d.png

Запускаем юнит тест и смотрим в дебаге на вычисления.

beec2775afbe4fb59ba8361c9819cf6f.png

Входные данные для пиксельного шейдера


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>


Ну и коротко о синтаксических отличиях:

  1. в теле шейдера используется Vec3f вместо vec3 (груви не удалось подружить с классом начинающимся с маленькой буквы)
  2. нет uniform — вместо них просто поля в шейдере
  3. нет varying, in, out — вместо них поля в классах, передаваемых в main


P.S. Проект я развиваю стихийно — то понадобится что-то, то просто интересно что-то сделать. Пока не успел сделать структуры и много чего другого. Если вам необходим какой-то функционал или направление развития (android? geometry shaders? kotlin?) — обращайтесь, обсудим!

Так же хочу выразить благодарность oshyshko и olexiy за помощь в написании статьи.

© Habrahabr.ru