Шейдеры

Если вы хотите работать с OpenGL ES 2.0, то вам следует знать некоторые основы шейдеров. Libgdx поставляется со стандартным шейдером, который позаботиться о визуализации некоторые вещей через SpriteBatch. Однако, если вы хотите сделать визуализацию полигональной сетки (Mesh) в Opengl ES 2.0, то вам нужно предоставить свой, правильный шейдер. В основном, вся визуализация в Opengl ES 2.0 делается с помощью шейдеров. Вот почему это называется программируемым конвейером. Мысль о вмешательстве в область шейдеров, может отпугнуть некоторых людей от использования ES 2.0, но стоит хорошо почитать о них, как шейдеры позволяют вам делать невероятно красивые вещи. Понять основы на самом деле довольно просто.

Что такое шейдеры

Шейдеры в OpenGL это небольшие программы, написанные на C подобном языке, называемым GLSL, который выполняется на графическом процессоре и обрабатывает данные необходимые для визуализации некоторые вещей. Шейдер может быть рассмотрен в простой форме, как стадия обработки на графическом процессоре. Он принимает набор входных данных, над которыми вы можете делать множество операции, и возвращает их обратно. Думайте об этот как о параметрах функции и возвращаемых значениях. Обычно, при визуализации чего-либо в OpenGL ES 2.0, данные передаются сначала через вершинные шейдеры, а затем через фрагментные (пиксельные) шейдеры.

Вершинные шейдеры

Исходя из названия, вершинные вейдеры отвечают за выполнение операций на вершинами. Более конкретно, каждое выполнение программы действует ровно на одну вершину. Это важная для понимания концепция. Все, что вы делаете в вершинном шейдере, происходит только ровно с одной вершиной.

Вот простой пример вершинного шейдера:

attribute vec4 a_position;

uniform mat4 u_projectionViewMatrix;

void main()
{
    gl_Position = a_position * u_projectionViewMatrix;
} 

Теперь все выглядит неплохо, не так ли? Сначала вы должны иметь атрибут вершины a_position. Этот атрибут является vec4, что означает вектор с 4 измерениями. Этом примере он содержит информацию о позиции вершины.

Далее у вас есть u_projectionViewMatrix. Этот матрица 4x4, которая содержим данные для преобразования проекции и вида. Если эти термины вам не ясны, то рекомендуется прочитать о перспективе и изометрической проекции.

Внутри main метода мы выполняем операции над вершиной. В данном случае, все что делает шейдер, это умножает позицию вершины на матрицу и присваивает результат gl_Position. gl_Position это ключевое слово предопределенное OpenGL и не может быть использовано для ничего, кроме передачи обработанной вершины.

Фрагментные шейдеры

Функции фрагментного шейдера очень похожи на вершинный шейдер, но вместо обработки вершин, он обрабатывает как каждый фрагмент один раз. Для простоты, считайте фрагмент как один пиксель. Теперь вы понимаете, что эта очень существенная разница.

Давайте предположим, что треугольник занимает площадь в 300 пикселей. Вершинный шейдер для этого треугольника будет выполнен 3 раза. Фрагментный шейдер будет выполнен 300 раз. Поэтому, имейте это в виду при написании шейдеров. Все что делается в фрагментном шейдере, будет экспоненциально более дороже.

Вот очень простой фрагментный шейдер:

void main()
{
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

Этот фрагментный шейдер будет просто отрисовывать каждый фрагмент сплошным красным цветом. gl_FragColor это другое предопределенное ключевое слово. Оно используется для вывода окончательного цвета фрагмента. Обратите внимание, как мы используем vec4(x,y,z,w), чтобы определить вектор внутри шейдера. В данном случае, вектор использован для определения цвета фрагмента.

ShaderProgram

Теперь у нас есть общее понимание того, что делает шейдер и как он работает. Давайте создадим его в libgdx. Это делается с помощью ShaderProgram класса. ShaderProgram состоит из вершинного и фрагментного шейдеров. Вы можете делать загрузку из файла или просто передать строку и содержать код шейдера внутри ваших java файлов.

Вот установка шейдера, с которой будем работать:

String vertexShader = "attribute vec4 a_position;    \n" + 
                      "attribute vec4 a_color;\n" +
                      "attribute vec2 a_texCoord0;\n" + 
                      "uniform mat4 u_worldView;\n" + 
                      "varying vec4 v_color;" + 
                      "varying vec2 v_texCoords;" + 
                      "void main()                  \n" + 
                      "{                            \n" + 
                      "   v_color = vec4(1, 1, 1, 1); \n" + 
                      "   v_texCoords = a_texCoord0; \n" + 
                      "   gl_Position =  u_worldView * a_position;  \n"      + 
                      "}                            \n" ;
String fragmentShader = "#ifdef GL_ES\n" +
                        "precision mediump float;\n" + 
                        "#endif\n" + 
                        "varying vec4 v_color;\n" + 
                        "varying vec2 v_texCoords;\n" + 
                        "uniform sampler2D u_texture;\n" + 
                        "void main()                                  \n" + 
                        "{                                            \n" + 
                        "  gl_FragColor = v_color * texture2D(u_texture, v_texCoords);\n"
                        "}";

Это довольно стандартная установка для шейдера, который использует атрибут позиции, атрибут цвета и атрибут текстурных координат. Обратите внимание на первые два изменения, производимые шейдером. Они выдают результат, который мы передаем в фрагментный шейдер.

В фрагментном шейдере у нас есть sampler2D, это специальный форма использующаяся для текстур. Как вы можете видеть в main функции, мы умножаем цвет вершины на найденный цвет из текстуры для получения окончательного вывода цвета.

Чтобы создать ShaderProgram мы делаем следующее:

ShaderProgram shader = new ShaderProgram(vertexShader, fragmentShader);

Мы можете убедиться в том, что шейдер скомпилирован правильно с помощью shader.isCompiled(). Лог компилирования можно вывести используя shader.getLog().

Мы также создаем соответствующую полигональную сетку и загружаем текстуру:

mesh = new Mesh(true, 4, 6, VertexAttribute.Position(), VertexAttribute.  ColorUnpacked(), VertexAttribute.TexCoords(0));
mesh.setVertices(new float[] 
{-0.5f, -0.5f, 0, 1, 1, 1, 1, 0, 1,
0.5f, -0.5f, 0, 1, 1, 1, 1, 1, 1,
0.5f, 0.5f, 0, 1, 1, 1, 1, 1, 0,
-0.5f, 0.5f, 0, 1, 1, 1, 1, 0, 0});
mesh.setIndices(new short[] {0, 1, 2, 2, 3, 0});
texture = new Texture(Gdx.files.internal("data/bobrgb888-32x32.png"));

В методе визуализации мы просто вызываем shader.begin() и передаем Uniform переменные, затем делаем визуализацию сетки с шейдером.

texture.bind();
shader.begin();
shader.setUniformMatrix("u_worldView", matrix);
shader.setUniformi("u_texture", 0);
mesh.render(shader, GL10.GL_TRIANGLES);
shader.end();

Хорошая вещь в шейдерах в OpenGL ES 2.0 в том, что вы имеете огромную доступную библиотеку шейдеров. Почти все, что делается в WebGL может быть легко перенесено на мобильные устройства.