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