Задний фон для игры с помощью шейдера на Defold

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

Задний фон для игры с помощью шейдера на Defold

Подготовка проекта.

Задний фон для игры с помощью шейдера на Defold

Для демонстрации создадим новый проект и выполним следующие шаги:

  • Скопировать текстуры в папку images.
  • Скопировать встроенные файлы sprite.material, sprite.fp (fragment program), sprite.vp (vertex program) в папку materials из buildins/materials
  • Добавить game object и model в главную коллекцию
  • Выбрать текстуры для модели.

Для того чтобы свойства модели отображали текстуры, нужно изменить материал, выбрав скопированные ранее sprite.fp, sprite.vp и добавить 5 семплеров

Задний фон для игры с помощью шейдера на Defold

Поставим для go размеры экрана и разместим по середине

Задний фон для игры с помощью шейдера на Defold

Шейдер

Откроем sprite.fp
Шейдер рисует пиксели выбирая их из текстуры texture_sampler по координатам var_texcoord0.xy, каждый пиксель будет умножен на tint для получения цвета и уровня прозрачности.

Задний фон для игры с помощью шейдера на Defold

Для того чтобы замостить текстурой всю поверхность несколько раз, нужно раздробить поверхность на секции, умножив var_texcoord0 на желаемое количество по горизонтали и вертикали:

vec2 uv = var_texcoord0.xy * 3.0;
gl_FragColor = texture2D(texture_sampler, uv) * tint_pm;

Задний фон для игры с помощью шейдера на Defold

Это не тот результат, которого мы хотим добиться, потому что мы проходим по координатам uv за пределы текстуры. Для того, чтобы возвращаться в начало, нужно брать значение по модулю. Для этого подходит функция fract(), которая отсекает целую часть.

gl_FragColor = texture2D(texture_sampler, fract(uv)) * tint_pm;

Задний фон для игры с помощью шейдера на Defold

Эти мордашки довольно растянутые, потому что размеры экрана 960х640, разделены на 3 части с сохранением пропорций экрана, но без учета размера текстуры. Зная размеры экрана и желаемый размер текстуры, можно получить количество секций, на которое следует делить поверхность.

lowp float pixels_per_unit = 300;
lowp vec2 resolution = vec2(960,640);
vec2 uv = vec2(
var_texcoord0.x * resolution.x / pixels_per_unit,
var_texcoord0.y * resolution.y / pixels_per_unit );

Задний фон для игры с помощью шейдера на Defold

Теперь выберем текстуры в зависимости от позиции в колонке и ряду

vec4 random_texture(in vec2 uv, in vec3 resolution)
{
vec2 id = floor(uv);
int index = int(id.x + id.y * resolution.x / resolution.z) % 5;

vec4 col = vec4(0);

if (index == 0)
col = texture2D(tex1,uv);
else if (index == 1)
col = texture2D(tex2,uv);
else if (index == 2)
col = texture2D(tex3,uv);
else if (index == 3)
col = texture2D(tex4,uv);
else
col = texture2D(tex5,uv); return col;
}

Использование функции

gl_FragColor = random_texture(uv, vec3(resolution, pixels_per_unit)) * tint_pm;

Задний фон для игры с помощью шейдера на Defold

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

Матрица трансформации масштаба:

mat2 scale(vec2 _scale) { return mat2(_scale.x,0.0,0.0,_scale.y); }

Масштаб применяется следующим образом:
uv = fract(uv) - vec2(0.5);
uv = scale( vec2(_scale) ) * uv;
uv = fract(uv) + vec2(0.5);

Задний фон для игры с помощью шейдера на Defold

Мы уменьшили размер изображения, но видим соседние части замощенной текстуры. Для того, чтобы оставить только нужное, можно создать маску.

Подойдет обычный квадрат, который должен быть также масштабирован

float box(vec2 _st, vec2 _size)
{

_size = vec2(0.5)-_size*0.5;
vec2 uv = step(_size,_st);
uv *= step(_size,vec2(1.0)-_st);
return uv.x*uv.y;
}

Там, где квадратная область возвращает цвет, альфа будет 1

vec2 box_uv = fract(uv) - vec2(0.5);
box_uv = scale( vec2(_scale) ) * box_uv;
box_uv = box_uv + vec2(0.5);
float alpha = box(box_uv,vec2(1));

Перед тем как вернуть текстуру, нужно применить к ней эту прозрачность

col.a *= alpha;

Задний фон для игры с помощью шейдера на Defold

Время случайных вращений! Функция вращения выглядит следующим образом:

vec2 rotate2D(vec2 _st, float _angle)
{
_st -= 0.5;
_st = mat2(cos(_angle),-sin(_angle), sin(_angle),cos(_angle)) * _st;
_st += 0.5; return _st;
}

Установим случайный угол

float rot = PI*random(id);

Функция рандом возвращает псевдослучайное значение

float random(vec2 st)
{
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}

Вращать нужно как саму текстуру, так и маску

float alpha = box(rotate2D(box_uv, rot),vec2(1));
uv = rotate2D(fract(uv),rot);

Задний фон для игры с помощью шейдера на Defold

Вращать можно не только отдельно текстуру, но и всю поверхность целиком, если трансформировать координаты сразу после разделения на секции.

vec2 uv = vec2(
var_texcoord0.x * resolution.x / pixels_per_unit,
var_texcoord0.y * resolution.y / pixels_per_unit );
uv = rotate2D(uv,PI*0.2);

Задний фон для игры с помощью шейдера на Defold

Осталось раскрасить в любимые цвета

Цвет фона:

Задний фон для игры с помощью шейдера на Defold

Цвет для зверьков:

Задний фон для игры с помощью шейдера на Defold

vec4 bg = vec4(0.21, 0.09, 0.41, 1);
vec4 mc = vec4(0.33, 0.08, 0.54, 1);

vec4 col = random_texture(uv, vec3(resolution, pixels_per_unit), 1.4);
if (col.a > 0)
gl_FragColor = col * mc * tint_pm;
else
gl_FragColor = bg * tint_pm;

Задний фон для игры с помощью шейдера на Defold

Параметры шейдера в defold

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

uniform lowp vec4 resolution;
uniform lowp vec4 bg;
uniform lowp vec4 mc;

Задний фон для игры с помощью шейдера на Defold

В game object, где лежит model можно добавить скрипт и менять параметры шейдера в runtime. На практике это полезно при изменении размера экрана.

Задний фон для игры с помощью шейдера на Defold
Задний фон для игры с помощью шейдера на Defold

Добавим динамики

мы можем добавить настройки для шейдера прямо в скрипт и передавать их в цикле update

Задний фон для игры с помощью шейдера на Defold

в редакторе это будет выглядеть так:

Задний фон для игры с помощью шейдера на Defold

Изменим как вычисляется rot в random_texture так, чтобы сделать rot динамичным. Нужно просто его умножать на текущее время и скорость, которые мы передаем из скриптапоскольку текстуры вычисляют случайное вращение, то умножение factor*=time*speed; дает случайную скорость для каждого тайла. Если хотим одну скорость вращения для всех тайлов, то фактор нужно просто добавить factor+=time*speed;

float time = time_0_rspeed_1_fspeed_2_separate_3.x;
float speed = time_0_rspeed_1_fspeed_2_separate_3.y;
bool separate = time_0_rspeed_1_fspeed_2_separate_3.w == 1;
float factor = random(id);
if (separate)
{
factor*=time*speed;
}
else
{
factor+=time*speed;
}
float rot = PI*factor;

Для того чтобы двигать все поле в функции main добавим смещение uv

float time = time_0_rspeed_1_fspeed_2_separate_3.x;
float speed = time_0_rspeed_1_fspeed_2_separate_3.z;
vec2 field_offset = vec2(move_field_direction.x, move_field_direction.y);

uv = uv+field_offset*speed*time;

Вот и все! все двигается и вращается и настройки в скрипте позволяют настраивать движение по своему вкусу.

Мой телеграм канал: https://t.me/pasha_gamedev

2121
13 комментариев

Ветвление по условию, зависящему от вершин/фрагментов, заставляет gpu выполнять оба потока инструкций. Получается, что для каждого пикселя этот шейдер будет читать данные из всех пяти текстур. Не проще было бы запаковать все спрайты в один атлас? Defold это предлагает из коробки.

1
Ответить

Спасибо, попробую так сделать

1
Ответить

Давно Дефолд мучаешь?

Ответить

Месяца 2 примерно. Делаю второй проект

Первый уже вышел на ЯИ
https://yandex.ru/games/app/222342?draft=true&lang=ru

1
Ответить

ОГОНЬ, БАТАРЕЯ!!! Побольше бы подобных постов и о движке в целом!!!

Ответить

Супиръ! 👍🏻

Ответить

Интересный подход, спасибо за то, что поделился! А почему было выбрано решение через фрагментный шейдер вместо использования отдельных спрайтов с вращением? Было бы сильно дешевле по производительности. Да и проще в реализации и контроле.

Ответить