Задний фон для игры с помощью шейдера на Defold
В качестве заднего фона для своей игры я решил использовать паттерн из игровых персонажей, то есть переиспользовать текстуры. Это позволило мне сэкономить на размере картинки (да мало, но все же), избавило от мук рисования или лицензирования изображения. А еще я получил новый навык.
Подготовка проекта.
Для демонстрации создадим новый проект и выполним следующие шаги:
- Скопировать текстуры в папку images.
- Скопировать встроенные файлы sprite.material, sprite.fp (fragment program), sprite.vp (vertex program) в папку materials из buildins/materials
- Добавить game object и model в главную коллекцию
- Выбрать текстуры для модели.
Для того чтобы свойства модели отображали текстуры, нужно изменить материал, выбрав скопированные ранее sprite.fp, sprite.vp и добавить 5 семплеров
Поставим для go размеры экрана и разместим по середине
Шейдер
Откроем sprite.fp
Шейдер рисует пиксели выбирая их из текстуры texture_sampler по координатам var_texcoord0.xy, каждый пиксель будет умножен на tint для получения цвета и уровня прозрачности.
Для того чтобы замостить текстурой всю поверхность несколько раз, нужно раздробить поверхность на секции, умножив var_texcoord0 на желаемое количество по горизонтали и вертикали:
vec2 uv = var_texcoord0.xy * 3.0;
gl_FragColor = texture2D(texture_sampler, uv) * tint_pm;
Это не тот результат, которого мы хотим добиться, потому что мы проходим по координатам uv за пределы текстуры. Для того, чтобы возвращаться в начало, нужно брать значение по модулю. Для этого подходит функция fract(), которая отсекает целую часть.
gl_FragColor = texture2D(texture_sampler, fract(uv)) * tint_pm;
Эти мордашки довольно растянутые, потому что размеры экрана 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 );
Теперь выберем текстуры в зависимости от позиции в колонке и ряду
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;
Теперь можно изменить индивидуальный размер текстуры, для того чтобы между ними появилось пространство.
Матрица трансформации масштаба:
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);
Мы уменьшили размер изображения, но видим соседние части замощенной текстуры. Для того, чтобы оставить только нужное, можно создать маску.
Подойдет обычный квадрат, который должен быть также масштабирован
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;
Время случайных вращений! Функция вращения выглядит следующим образом:
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);
Вращать можно не только отдельно текстуру, но и всю поверхность целиком, если трансформировать координаты сразу после разделения на секции.
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);
Осталось раскрасить в любимые цвета
Цвет фона:
Цвет для зверьков:
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
Для того, чтобы передавать значения цвета, размера текстуры и размера экрана в шейдер, можно добавить переменные в материал, объявив их как uniform в коде
uniform lowp vec4 resolution;
uniform lowp vec4 bg;
uniform lowp vec4 mc;
В game object, где лежит model можно добавить скрипт и менять параметры шейдера в runtime. На практике это полезно при изменении размера экрана.
Добавим динамики
мы можем добавить настройки для шейдера прямо в скрипт и передавать их в цикле update
в редакторе это будет выглядеть так:
Изменим как вычисляется 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://github.com/busovikov/Shader-Showroom
Мой телеграм канал: https://t.me/pasha_gamedev
Ветвление по условию, зависящему от вершин/фрагментов, заставляет gpu выполнять оба потока инструкций. Получается, что для каждого пикселя этот шейдер будет читать данные из всех пяти текстур. Не проще было бы запаковать все спрайты в один атлас? Defold это предлагает из коробки.
Спасибо, попробую так сделать
Давно Дефолд мучаешь?
Месяца 2 примерно. Делаю второй проект
Первый уже вышел на ЯИ
https://yandex.ru/games/app/222342?draft=true&lang=ru
ОГОНЬ, БАТАРЕЯ!!! Побольше бы подобных постов и о движке в целом!!!
Супиръ! 👍🏻
Интересный подход, спасибо за то, что поделился! А почему было выбрано решение через фрагментный шейдер вместо использования отдельных спрайтов с вращением? Было бы сильно дешевле по производительности. Да и проще в реализации и контроле.