Отладка шейдеров на 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> 

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

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

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/269591/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *