May 1, 2023

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

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

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

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

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

Для того чтобы свойства модели отображали текстуры, нужно изменить материал, выбрав скопированные ранее 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. На практике это полезно при изменении размера экрана.

Код fp целиком

varying mediump vec2 var_texcoord0;

uniform lowp sampler2D tex1;
uniform lowp sampler2D tex2;
uniform lowp sampler2D tex3;
uniform lowp sampler2D tex4;
uniform lowp sampler2D tex5;
uniform lowp vec4 tint;
uniform lowp vec4 resolution;
uniform lowp vec4 bg;
uniform lowp vec4 mc;

#define PI 3.14159265358979323846

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

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;
}

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

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;
}

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

    vec4 col = vec4(0);

    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(rotate2D(box_uv, rot),vec2(1));
    uv = rotate2D(fract(uv),rot);

    uv = fract(uv) - vec2(0.5);
    uv = scale( vec2(_scale) ) * uv;
    uv = fract(uv) + vec2(0.5);

    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);

    col.a *= alpha;
    
    return col;
}

void main()
{
    vec2 uv = vec2(var_texcoord0.x * resolution.x / resolution.z, var_texcoord0.y * resolution.y / resolution.z);
    uv = rotate2D(uv,PI*0.2);
    lowp vec4 tint_pm = vec4(tint.xyz * tint.w, tint.w);

    vec4 col = random_texture(uv, vec3(resolution.xy, resolution.z), 1.4);
    if (col.a > 0)
    gl_FragColor =  col * mc * tint_pm;
    else
    gl_FragColor = bg * tint_pm; //chess(botom_color, top_color, uv);
}

Проект целиком: https://github.com/busovikov/Shader-Showroom

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