Добавление ColorKey в libGDX

в 14:36, , рубрики: android, libgdx, игры, разработка игр, разработка мобильных приложений, Разработка под android, шейдеры

Привет! В данной заметке я расскажу о добавлении colorkey в библиотеку libgdx (или любую другую, где есть шейдеры). Как известно, нативной поддержки «прозрачного цвета» в libgdx нет, поэтому приходится хранить полноцветное изображение в формате RGBA8888 (4 байта на пиксель), либо в усечённом формате RGBA4444 (2 байта на пиксель), который позволяет вдвое уменьшить использование памяти, но сильно ухудшает картинку. При создании 2D игр, зачастую, было бы достаточно всего одного бита прозрачности… Особенно на мобильных платформах… но его нет… Сделаем, чтобы был!

Добавление ColorKey в libGDX - 1

RGBA8888

Для начала нам понадобится эталонное изображение в формате RGBA8888, с которым будут сравниваться все последующие попытки сэкономить байты. Экспериментировать будем с набором тайлов, из которых рисуется уровень, на небо и человечка внимания не обращаем. Размер тайлсета 512 а 512 пикселов. На диске текстуры сохранены в 8-bit png с прозрачностью и занимают 20 килобайт (можно сохранить в в 32 битный png, с тем же результатом, т.к. графика простая, а прозрачность всегда либо есть, либо её нет). В видеопамяти же они займут уже 512*512*4 = 1 мегабайт ровно. Хоцца поменьше, это же не единственная текстура...

Добавление ColorKey в libGDX - 2

RGBA4444

Перво-наперво возникает мысль использовать усечённую разрядность. Пиксельарт простой, цветов мало, а сэкономим сразу 512 килобайт. Пробуем:

Добавление ColorKey в libGDX - 3

Трава лишь немного изменила оттенок, с этим можно смириться, но вот камни пострадали критически. Если вы и с этим готовы смириться, то дальше можно не читать. Я же не готов.

Пишем шейдер!

Не мудрствуя лукаво я скопировал шейдеры по умолчанию и модифицировал фрагментный шейдер:

	private static String fragmentShader = "#ifdef GL_ESn" //
			+ "#define LOWP lowpn" //
			+ "precision mediump float;n" //
			+ "#elsen" //
			+ "#define LOWP n" //
			+ "#endifn" //
			+ "varying LOWP vec4 v_color;n" //
			+ "varying vec2 v_texCoords;n" //
			+ "uniform sampler2D u_texture;n" //
			+ "void main()n"//
			+ "{n" //
			+ "  LOWP vec4 pixel = texture2D(u_texture, v_texCoords);n"//
			+ "  gl_FragColor = v_color * pixel;n" //
			+ "  if( pixel.rgb == vec3(1.0, 0.0, 1.0) ){gl_FragColor.a = 0.0;}n"//
			+ "}";

Интересны только последние три строчки. Сперва сохраняем цвет текселя (Важно! Интерполяция текстуры должна быть отключена, т.е. при загрузке используем фильтрацию NEAREST). Затем задаём цвет пикселя, умножая цвет текселя на цвет вершины. Если вы не примешиваете цвета вершин, то это умножение можно заменить на присваивание. И, наконец, сравниваем цвет текселя с «прозрачным цветом» и, если цвета совпадают, то делаем пиксель прозрачным. В качестве «прозрачного» я выбрал классический вырвиглазно-пурпурный rgb(255,0,255). Наверняка от условного оператора можно избавиться, но… «И так сойдёт!».)

RGB565

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

Добавление ColorKey в libGDX - 4

Вот так мы легко и непринуждённо уменьшили потребление памяти вдвое, практически без потерь качества и скорости (всё-таки от условного оператора в шейдере хочется избавиться). Но, хочется большего. Хочется сжать текстуры в формат ETC1, но с прозрачностью. Всё-таки в шесть раз меньше занимает, чем RGB. и не на диске, а в памяти!

Пробуем… Эпик фэйл. Ожидаемый. Заглавная картинка как раз результат данной попытки. Результат ожидаемый, ведь ETC1 — формат сжатия с потерями. С сильными потерями. Вырвиглазный цвет помутнел и появились пикселы полувырвиглазного цвета. Обычно, альфа-канал хранят в отдельной текстуре. Часто — без сжатия. Но это не наш метод! Давайте посмотрим, чего можно добиться, если немного пошалить с шейдером.

Шейдер для затейников

 if( vec3( min(pixel.r,0.95), max(pixel.g,0.05), min(pixel.b,0.95)) == vec3(0.95, 0.05, 0.95) )
 {
    gl_FragColor.a = 0.0;
 }

Заменим только последнюю строчку в нашем шейдере. Теперь мы сравниваем не строго с конкретным цветом, а с небольшим отклонением: красной и синей компоненте разрешаем быть немного темнее, а зелёной — светлее.

Добавление ColorKey в libGDX - 5

Тут даже сравнивать с оригиналом не нужно, артефакты видны не вооружённым глазом. Но! Если поиграться с допустимым отклонением, или считать «расстояние» между цветами (тоже с достаточным отклонением), то вполне можно добиться сносных результатов для конкретного набора текстур. Когда борешься за каждый килобайт — этот способ может оказаться вполне приемлемым.

Прозрачный jpeg?

А почему нет? У нас уже есть шеёдер, который сделает прозрачным любую тестуру. Если повезёт, то результат даже будет пригодным к использованию. Если важно занимаемое на диске место, а png слишком плохо жмёт, то почему бы и нет. Попробуем сразу два варианта: с профилем сжатия «maximum» и «very high»

Добавление ColorKey в libGDX - 6

Видим, что с профилем «maximum» вполне возможно использование jpg с «прозрачным цветом». Теоретически. Если использование png оказывается менее выгодным.

Итак, у нас получилось вдвое уменьшить занимаемую память, почти не потеряв в красочности тексур, но получив «прозрачный цвет» для указания полностью прозрачных областей. В качестве бонуса, научились делать прозрачный jpg.

Надеюсь, заметка будет полезна не только мне. Ещё больше надеюсь, что кто-нибудь предложит эквивалентный код без условного оператора. Спасибо за внимание.

UPD:

Пользователь FadeToBlack предложил два варианта шейдера без условного оператора:

Этот шейдер можно использовать только с текстурами, в которых прозрачность указана через цветовой ключ. Текстуры с реальной прозрачностью будут отображаться не правильно. Шейдер из статьи корректно обрабатывает как текстуры с реальной прозрачностью, так и с «прозрачным цветом».

			void main()
			{
			     LOWP vec4 pixel = texture2D(u_texture, v_texCoords);
			     gl_FragColor = v_color * pixel;
			     gl_FragColor.a = float(pixel.rgb != vec3(1.0,0.0,1.0));
			}

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

			void main()
			{
			     LOWP vec4 pixel = texture2D(u_texture, v_texCoords);
			     gl_FragColor = v_color * pixel;
			     gl_FragColor.a = gl_FragColor.a * float(pixel.rgb != vec3(1.0,0.0,1.0));
			}

Автор: maaGames

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js