Задний фон для игры с помощью шейдера на 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;
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));
Перед тем как вернуть текстуру, нужно применить к ней эту прозрачность
Время приключений случайных вращений! Функция вращения выглядит следующим образом:
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 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. На практике это полезно при изменении размера экрана.
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