Пользователь
0,0
рейтинг
11 января в 11:15

Разработка → Шейдеры в libgdx для чайников из песочницы

Статья будет полезна тем, кто начинает свое знакомство с libgdx и шейдерами. Шейдеры часто игнорируются новичками, хотя и позволяют делать множество красивых эффектов, достаточно просто. Я не буду сильно углубляться в OpenGL и шейдеры, а пройдусь лишь по верхам, но этого вполне достаточно как для использования чужих шейдеров, так и для написания своих.

Немного теории


Итак, что такое шейдер? Шейдеры в OpenGL — это небольшие программы, написанные на C подобном языке GLSL. Эти программы исполняются напрямую на GPU. Шейдеры работают в паре: вершинные шейдеры и фрагментные.



Вершинный шейдер (vertex shader) отвечает за выполнение операций над вершинами. Каждое выполнение программы действует ровно на одну вершину. Если посмотреть на рисунок треугольника, то у него 3 вершины, соответственно вершинный шейдер выполнится 3 раза. Вершинный шейдер задаст конечные позиции вершин с учетом положения камеры, а так же подготовит и выведет некоторые переменные, требуемые для фрагментного шейдера. При разработке простых шейдеров, вам скорее всего не понадобится изменять вершинный шейдер.

Фрагментный шейдер (fragment shader) обрабатывает каждую видимую часть конечного изображения. Я буду называть каждый такой фрагмент пикселем, хотя это не совсем верно, так как пиксель в рендеринге OpenGL и в итоговом изображении, которое вы видите на экране, может различаться по размеру.

Внутри фрагментного шейдера мы будем работать со всем, что связано с поверхностью — освещение, тени, отражения, текстуры и любые эффекты, которые вы захотите. Результат работы фрагментного шейдера — это цвет пикселя в формате RGBA (красный, зеленый, синий и альфа-канал). Для большинства эффектов мы будем изменять именно его.

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

Стандартные шейдеры в libgdx


Прежде чем приступить к стандартным шейдерам, еще немного теории. Язык GLSL — это C подобный язык, и я не буду заострять внимание на базовых вещах, однако есть вещи, которые я должен пояснить, прежде чем мы начнем разбирать код.

В шейдерах используются такие понятия как: attribute, uniform, varying.

Атрибуты (attribute) — это свойство вершины. У вершины могут быть различные атрибуты. Например, координаты положения в пространстве, координаты вектора нормали, цвет. Кроме того, вы можете передавать в вершинный шейдер какие-либо свои атрибуты. Важно понять, что атрибут — это свойство вершины, и поэтому он должен быть задан для каждой вершины. Атрибуты передаются в только вершинный шейдер. Атрибуты доступны вершинному шейдеру только для чтения и не могут быть перезаписаны.

Юниформы (uniform) — это внешние данные, которые могут быть использованы для расчетов, но не могут быть перезаписаны. Униформы могут быть переданы как в вершинный, так и во фрагментный шейдеры. Униформы никак не связаны с конкретной вершиной и являются глобальными константами. Например, в качестве униформ можно передать в шейдер координаты источника света и координаты глаза (камеры).

Переменные (varying) — это данные, которые при переходе из вершинного во фрагментный шейдер будут вычислены для каждого пикселя путем усреднения данных вершин. Поясню подробнее. В вершинном шейдере мы имеем дело с координатами конкретной вершины. Если передать координаты этой вершины в фрагментый шейдер как varying, то на входе фрагментного шейдера получим координаты в пространстве уже для каждого пикселя, которые будут получены путем усреднения координат вершин. Процесс усреднения называют интерполяцией. Аналогично интерполируются координаты вектора нормали и координаты вектора цвета. Важно, что varying-переменные должны быть обязательно объявлены одинаково в вершинном и фрагментном шейдерах.

Вершинный шейдер
attribute vec4 a_position; //позиция вершины
attribute vec4 a_color; //цвет вершины
attribute vec2 a_texCoord0; //координаты текстуры
uniform mat4 u_projTrans;  //матрица, которая содержим данные для преобразования проекции и вида
varying vec4 v_color;  //цвет который будет передан в фрагментный шейдер
varying vec2 v_texCoords;  //координаты текстуры
void main(){
    v_color=a_color;
    // При передаче цвет из SpriteBatch в шейдер, происходит преобразование из ABGR int цвета в float. 
    // что-бы избежать NAN  при преобразование, доступен не весь диапазон для альфы, а только значения от (0-254)
    //чтобы полностью передать непрозрачность цвета, когда альфа во float равна 1, то всю альфу приходится умножать.
    //это специфика libgdx и о ней надо помнить при переопределение  вершинного шейдера.
    v_color.a = v_color.a * (255.0/254.0);
    v_texCoords = a_texCoord0;
    //применяем преобразование вида и проекции, можно не забивать себе этим голову
    // тут происходят математические преобразование что-бы правильно учесть параметры камеры
    // gl_Position это окончательная позиция вершины 
    gl_Position =  u_projTrans * a_position; 
}


Фрагментный шейдер
//#ifdef позволяет коду работать на слабых телефонах, и мощных пк.Если шейдер используется на телефоне(GL_ES) то  
//используется низкая разрядность (точность) данных.(highp – высокая точность; mediump – средняя точность; lowp – низкая точность)
#ifdef GL_ES   
    #define LOWP lowp
    precision mediump float;
#else
    #define LOWP
#endif
varying LOWP vec4 v_color;
varying vec2 v_texCoords;
// sampler2D это специальный формат данных в  glsl для доступа к текстуре
uniform sampler2D u_texture;
void main(){
    gl_FragColor = v_color * texture2D(u_texture, v_texCoords);// итоговый цвет пикселя
}


Работа с шейдерами в Libgdx


В libgdx для работы с шейдерами используется класс ShaderProgram.На вход он принимает либо два файла, либо две строки содержащих код шейдеров.

//Загрузка из файлов
shaderProgram=new ShaderProgram(Gdx.files.internal("shaders/default.vert"),Gdx.files.internal("shaders/default.frag"));
//Загрузка из строк vertexShader и fragmentShader это String в котором хранится код шейдеров
shaderProgram=new ShaderProgram(vertexShader,fragmentShader);

При работе с шейдерами желательно написать:

ShaderProgram.pedantic = false;

Потому что без этого шейдеры могут не скомпилироваться, libgdx ругается когда в шейдере есть юниформы которые не используются. После того как шейдер стал не нужен, важно не забыть освободить ресурсы:

shaderProgram.dispose().

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

//как и в стандартном шейдере получаем итоговый цвет пикселя
gl_FragColor = v_color * texture2D(u_texture, v_texCoords);
//после получения итогового цвета, меняем его на противоположный
gl_FragColor.rgb=1.0-gl_FragColor.rgb;

Результат, который мы хотим получить


Фрагментный шейдер
#ifdef GL_ES
    #define LOWP lowp
    precision mediump float;
#else
    #define LOWP
#endif
varying LOWP vec4 v_color;
varying vec2 v_texCoords;
uniform sampler2D u_texture;
void main(){
    //как и в стандартном шейдере получаем итоговый цвет пикселя
    gl_FragColor = v_color * texture2D(u_texture, v_texCoords);
    //после получения итогового цвета, меняем его на противоположный
    gl_FragColor.rgb=1.0-gl_FragColor.rgb;
}


Итоговый код
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.scenes.scene2d.Stage;

public class ShaderDemo extends ApplicationAdapter {

	SpriteBatch batch;
	Texture img;
	ShaderProgram shader;

	@Override
	public void create() {
		batch = new SpriteBatch();
		img = new Texture("badlogic.jpg");

		//желательно использовать, тк если мы используем не все юниформы, то шейдер не скомпилируется
		ShaderProgram.pedantic = false;
		shader = new ShaderProgram(Gdx.files.internal("shaders/default.vert"), 
				(Gdx.files.internal("shaders/invertColors.frag")));
		if (!shader.isCompiled()) {
			System.err.println(shader.getLog());
			System.exit(0);
		}
		batch = new SpriteBatch(1000);
		batch.setShader(shader);
	}
	@Override
	public void render() {
		Gdx.gl.glClearColor(1, 0, 0, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
		batch.begin();
		batch.draw(img, 0, 0,Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		batch.end();
	}
	
	@Override
	public void dispose() {
                //важно не забыть освободить память от шейдера,когда он больше не нужен
		batch.dispose();
		shader.dispose();
		img.dispose();
	}
}

@d954mas
карма
9,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (7)

  • +2
    Стоит добавить, что в libgdx все преобразования (поворот и прочее) реализует на шейдерах, включая работу с вьюпортом (вроде бы, не уверен), например. т.е. у каждого элемента уже присобачен внутри дефолтный шейдер, его стоит выдирать из ядра и дописывать, иначе поломается некоторая функциональность. Именно с этим и связаны зачастую ошибки, которые заставляют ставить pedantic = false поле.

    Лично я поседел, пока не распотрошил все исходники libgdx и не понял почему у меня отваливается половина функционала при добавлении шейдера. Так что надеюсь эта информация будет для вас полезной.
    • 0
      P.S. Немного промахнулся (запамятовал), не в спрайтах, а в батче, вот сами сырцы: github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/SpriteBatch.java#L127 Т.е. присобачивание нового убивает существующий и начинает творится всякая магия.
    • 0
      Вполне закономерно: в libgdx есть готовый шейдер для рисования спрайтов в батч-режиме. Для этого SpriteBatch передает в этот шейдер определенные параметры, если поменять шейдер, то эти параметры не поменяются. Так что если хочется использовать SpriteBatch, нужно свой кастомный шейдер писать таким образом, чтобы он поддерживал предопределенные SpriteBatch-ем параметры. Или не использовать его, а использовать шейдеры напрямую.
  • +1
    Мне кажется, тема недостаточно раскрыта.
    Во-первых, использование batch при рисовании скрывает особенности того, как это всё работает. Для понимания происходящего было бы полезно вручную задать юниформы, атрибуты и вызвать функцию рисования.
    Во-вторых, описание вершинного шейдера тоже не способствует понимаю. Я бы посоветовал новичкам написать gl_Position = a_position; и поиграться с разными значениями атрибутов, чтобы хорошо представлять, как это всё работает. (от -1 до 1 по x и y, при этом координаты однородные и на самом деле используются значения x/w, y/w).
    В принципе, если кому-то нужно — могу написать небольшую статью.
    • 0
      К слову о новичках в мире шейдеров, хоть и небольшой оффтоп, но мне очень сильно понравилась книга Коичи Мацуда в соавторстве с Роджером Ли: «WegGL: Программирование трёхмерной графики». Она по JS, но там для меня нашлось очень много полезной информации исключительно по шейдерам в мире OpenGL. Собственно по ней я и разбирался в шейдерах и устройстве libgdx (как бы это не комично звучало).
    • 0
      Было бы интересно прочитать об этом более подробно.Я специально использовал batch, чтобы не передавать атрибуты для вершин вручную, по моему это только путает и пугает на начальном этапе.Насчет юниформ, согласен, надо было выбрать другой шейдер хотя-бы с одной юниформой, я как-то не подумал об этом.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.