Как правильно кушать шейдеры в Game Maker Studio 2 и не подавиться
Речь пойдёт в том числе и о проблеме, с которой я столкнулся в процессе адаптации моей игры Sig.NULL под FullHD разрешение для релиза на XBOX. Я решил переделать графику под современные реалии, чтоб не так позорно и стыдно было.
Но на самом деле это просто повод рассказать о том как я научился применять шейдеры на уровне копипасты и небольшого допила ручками. Так что здесь будет изложено решение основной проблемы и мои комментарии как это работает.
Я раньше считал что шейдеры — это кусачая тема, где без знания матанализа и линейной алгебры никуда.
Но нет, нифига. Простые вещи в духе перекраски и обесцвечивания можно сделать своими силами, если понимать как работает rgb палитра и знать что такое координатная плоскость.
Здесь практически не будет картинок, в основном текст и код — для тех кто хочет как и я погрузиться в мир шейдеров используя Game Maker Studio 2, но уже по протоптанной мной дорожке. Пользователям других движков тоже может быть полезно, но шейдеры — это такая штука language specific, как говорится. То есть в разных средах разработки может применяться по-разному.
В последнее время я часто графику прототипирую в фотошопе, и часто использую режимы смешивания чтобы достичь тех или иных результатов. Бывает так, что штатные режимы смешивания в движке грешат. Ну вот есть допустим режим смешивания overlay, в том же Game Maker Studio 2 можно добиться ПРИМЕРНО того же самого через gpu_set_blend_mode.
Примерно… ПРИМЕРНО!
Но не совсем того. Например, белые пиксели он замешивает с цветом оверлея, а в фотошопе они остаются белыми.
Я покопался в инете и нашёл GLSL ES шейдер на все режимы смешивания как в фотошопе
vec4 Desaturate (vec3 color, float Desaturation)
{
vec3 grayXfer = vec3(0.3, 0.59, 0.11);
vec3 gray = vec3(dot (grayXfer, color));
return vec4(mix (color, gray, Desaturation), 1.0);
}
/*
** Hue, saturation, luminance
*/
vec3 RGBToHSL (vec3 color)
{
vec3 hsl; // init to 0 to avoid warnings? (and reverse if + remove first part)
float fmin = min (min (color.r, color.g), color.b); //Min. value of RGB
float fmax = max (max (color.r, color.g), color.b); //Max. value of RGB
float delta = fmax — fmin; //Delta RGB value
hsl.z = (fmax + fmin) / 2.0; // Luminance
if (delta == 0.0) //This is a gray, no chroma…
{
hsl.x = 0.0; // Hue
hsl.y = 0.0; // Saturation
}
else //Chromatic data…
{
if (hsl.z < 0.5)
hsl.y = delta / (fmax + fmin); // Saturation
else
hsl.y = delta / (2.0 — fmax — fmin); // Saturation
float deltaR = (((fmax — color.r) / 6.0) + (delta / 2.0)) / delta;
float deltaG = (((fmax — color.g) / 6.0) + (delta / 2.0)) / delta;
float deltaB = (((fmax — color.b) / 6.0) + (delta / 2.0)) / delta;
if (color.r == fmax)
hsl.x = deltaB — deltaG; // Hue
else if (color.g == fmax)
hsl.x = (1.0 / 3.0) + deltaR — deltaB; // Hue
else if (color.b == fmax)
hsl.x = (2.0 / 3.0) + deltaG — deltaR; // Hue
if (hsl.x < 0.0)
hsl.x += 1.0; // Hue
else if (hsl.x > 1.0)
hsl.x -= 1.0; // Hue
}
return hsl;
}
float HueToRGB (float f1, float f2, float hue)
{
if (hue < 0.0)
hue += 1.0;
else if (hue > 1.0)
hue -= 1.0;
float res;
if ((6.0 * hue) < 1.0)
res = f1 + (f2 — f1) * 6.0 * hue;
else if ((2.0 * hue) < 1.0)
res = f2;
else if ((3.0 * hue) < 2.0)
res = f1 + (f2 — f1) * ((2.0 / 3.0) — hue) * 6.0;
else
res = f1;
return res;
}
vec3 HSLToRGB (vec3 hsl)
{
vec3 rgb;
if (hsl.y == 0.0)
rgb = vec3(hsl.z); // Luminance
else
{
float f2;
if (hsl.z < 0.5)
f2 = hsl.z * (1.0 + hsl.y);
else
f2 = (hsl.z + hsl.y) — (hsl.y * hsl.z);
float f1 = 2.0 * hsl.z — f2;
rgb.r = HueToRGB (f1, f2, hsl.x + (1.0/3.0));
rgb.g = HueToRGB (f1, f2, hsl.x);
rgb.b= HueToRGB (f1, f2, hsl.x — (1.0/3.0));
}
return rgb;
}
/*
** Contrast, saturation, brightness
** Code of this function is from TGM’s shader pack
** irrlicht.sourceforge.net/phpBB2/viewtopic.php?t=21057
*/
// For all settings: 1.0 = 100% 0.5=50% 1.5 = 150%
vec3 ContrastSaturationBrightness (vec3 color, float brt, float sat, float con)
{
// Increase or decrease theese values to adjust r, g and b color channels seperately
const float AvgLumR = 0.5;
const float AvgLumG = 0.5;
const float AvgLumB = 0.5;
const vec3 LumCoeff = vec3(0.2125, 0.7154, 0.0721);
vec3 AvgLumin = vec3(AvgLumR, AvgLumG, AvgLumB);
vec3 brtColor = color * brt;
vec3 intensity = vec3(dot (brtColor, LumCoeff));
vec3 satColor = mix (intensity, brtColor, sat);
vec3 conColor = mix (AvgLumin, satColor, con);
return conColor;
}
/*
** Float blending modes
** Adapted from here: www.nathanm.com/photoshop-blending-math/
** But I modified the HardMix (wrong condition), Overlay, SoftLight, ColorDodge, ColorBurn, VividLight, PinLight (inverted layers) ones to have correct results
*/
#define BlendLinearDodgef BlendAddf
#define BlendLinearBurnf BlendSubstractf
#define BlendAddf (base, blend) min (base + blend, 1.0)
#define BlendSubstractf (base, blend) max (base + blend — 1.0, 0.0)
#define BlendLightenf (base, blend) max (blend, base)
#define BlendDarkenf (base, blend) min (blend, base)
#define BlendLinearLightf (base, blend) (blend < 0.5? BlendLinearBurnf (base, (2.0 * blend)) : BlendLinearDodgef (base, (2.0 * (blend — 0.5))))
#define BlendScreenf (base, blend) (1.0 — ((1.0 — base) * (1.0 — blend)))
#define BlendOverlayf (base, blend) (base < 0.5? (2.0 * base * blend) : (1.0 — 2.0 * (1.0 — base) * (1.0 — blend)))
#define BlendSoftLightf (base, blend) ((blend < 0.5)? (2.0 * base * blend + base * base * (1.0 — 2.0 * blend)) : (sqrt (base) * (2.0 * blend — 1.0) + 2.0 * base * (1.0 — blend)))
#define BlendColorDodgef (base, blend) ((blend == 1.0)? blend : min (base / (1.0 — blend), 1.0))
#define BlendColorBurnf (base, blend) ((blend == 0.0)? blend : max ((1.0 — ((1.0 — base) / blend)), 0.0))
#define BlendVividLightf (base, blend) ((blend < 0.5)? BlendColorBurnf (base, (2.0 * blend)) : BlendColorDodgef (base, (2.0 * (blend — 0.5))))
#define BlendPinLightf (base, blend) ((blend < 0.5)? BlendDarkenf (base, (2.0 * blend)) : BlendLightenf (base, (2.0 *(blend — 0.5))))
#define BlendHardMixf (base, blend) ((BlendVividLightf (base, blend) < 0.5)? 0.0 : 1.0)
#define BlendReflectf (base, blend) ((blend == 1.0)? blend : min (base * base / (1.0 — blend), 1.0))
/*
** Vector3 blending modes
*/
// Component wise blending
#define Blend (base, blend, funcf) vec3(funcf (base.r, blend.r), funcf (base.g, blend.g), funcf (base.b, blend.b))
#define BlendNormal (base, blend) (blend)
#define BlendLighten BlendLightenf
#define BlendDarken BlendDarkenf
#define BlendMultiply (base, blend) (base * blend)
#define BlendAverage (base, blend) ((base + blend) / 2.0)
#define BlendAdd (base, blend) min (base + blend, vec3(1.0))
#define BlendSubstract (base, blend) max (base + blend — vec3(1.0), vec3(0.0))
#define BlendDifference (base, blend) abs (base — blend)
#define BlendNegation (base, blend) (vec3(1.0) — abs (vec3(1.0) — base — blend))
#define BlendExclusion (base, blend) (base + blend — 2.0 * base * blend)
#define BlendScreen (base, blend) Blend (base, blend, BlendScreenf)
#define BlendOverlay (base, blend) Blend (base, blend, BlendOverlayf)
#define BlendSoftLight (base, blend) Blend (base, blend, BlendSoftLightf)
#define BlendHardLight (base, blend) BlendOverlay (blend, base)
#define BlendColorDodge (base, blend) Blend (base, blend, BlendColorDodgef)
#define BlendColorBurn (base, blend) Blend (base, blend, BlendColorBurnf)
#define BlendLinearDodge BlendAdd
#define BlendLinearBurn BlendSubstract
// Linear Light is another contrast-increasing mode
// If the blend color is darker than midgray, Linear Light darkens the image by decreasing the brightness. If the blend color is lighter than midgray, the result is a brighter image due to increased brightness.
#define BlendLinearLight (base, blend) Blend (base, blend, BlendLinearLightf)
#define BlendVividLight (base, blend) Blend (base, blend, BlendVividLightf)
#define BlendPinLight (base, blend) Blend (base, blend, BlendPinLightf)
#define BlendHardMix (base, blend) Blend (base, blend, BlendHardMixf)
#define BlendReflect (base, blend) Blend (base, blend, BlendReflectf)
#define BlendGlow (base, blend) BlendReflect (blend, base)
#define BlendPhoenix (base, blend) (min (base, blend) — max (base, blend) + vec3(1.0))
#define BlendOpacity (base, blend, F, O) (F (base, blend) * O + base* (1.0 — O))
// Hue Blend mode creates the result color by combining the luminance and saturation of the base color with the hue of the blend color.
vec3 BlendHue (vec3 base, vec3 blend)
{
vec3 baseHSL = RGBToHSL (base);
return HSLToRGB (vec3(RGBToHSL (blend).r, baseHSL.g, baseHSL.b));
}
// Saturation Blend mode creates the result color by combining the luminance and hue of the base color with the saturation of the blend color.
vec3 BlendSaturation (vec3 base, vec3 blend)
{
vec3 baseHSL = RGBToHSL (base);
return HSLToRGB (vec3(baseHSL.r, RGBToHSL (blend).g, baseHSL.b));
}
// Color Mode keeps the brightness of the base color and applies both the hue and saturation of the blend color.
vec3 BlendColor (vec3 base, vec3 blend)
{
vec3 blendHSL = RGBToHSL (blend);
return HSLToRGB (vec3(blendHSL.r, blendHSL.g, RGBToHSL (base).b));
}
// Luminosity Blend mode creates the result color by combining the hue and saturation of the base color with the luminance of the blend color.
vec3 BlendLuminosity (vec3 base, vec3 blend)
{
vec3 baseHSL = RGBToHSL (base);
return HSLToRGB (vec3(baseHSL.r, baseHSL.g, RGBToHSL (blend).b));
}
/*
** Gamma correction
** Details: blog.mouaif.org/2009/01/22/photoshop-gamma-correction-shader/
*/
#define GammaCorrection (color, gamma) pow (color, 1.0 / gamma)
/*
** Levels control (input (+gamma), output)
** Details: blog.mouaif.org/2009/01/28/levels-control-shader/
*/
#define LevelsControlInputRange (color, minInput, maxInput) min (max (color — vec3(minInput), vec3(0.0)) / (vec3(maxInput) — vec3(minInput)), vec3(1.0))
#define LevelsControlInput (color, minInput, gamma, maxInput) GammaCorrection (LevelsControlInputRange (color, minInput, maxInput), gamma)
#define LevelsControlOutputRange (color, minOutput, maxOutput) mix (vec3(minOutput), vec3(maxOutput), color)
#define LevelsControl (color, minInput, gamma, maxInput, minOutput, maxOutput) LevelsControlOutputRange (LevelsControlInput (color, minInput, gamma, maxInput), minOutput, maxOutput)
Т.к. я пользую GameMakerStudio 2, то расскажу как применять именно в этом конструкторе.
Пример 1 — подкрашивание
Начну с примера попроще. Допустим, нужно наложить какой-то цвет поверх спрайта с режимом смешивания. У моих дронов в игре есть «глаза», которые я хочу красить в цвет дрона, посмотрим как это делается.
Есть в Game Maker Studio 2 встроенное смешивание с цветом, которое мой прекрасный глаз дрона целиком заливает цветом. А я хочу чтобы блик остался! На помощь придёт шейдер!
Сперва нужно создать дефолтный шейдер (он по стандарту GLSL ES если что)
Открывается две вкладки:
- вершинный шейдер (о котором будет в третьем примере) — занимается пикселизацией, то есть векторное превращает в пиксельное. Например, 3Д модель покрыта текстурой, происходит разбивка поврехности модели на треугольники, и для каждого треугольника определяется то, какой стороной он повёрнут к камере и как это вывести на экран и какая часть текстуры попадает на этот треугольник.
- пиксельный шейдер — обрабатывает саму текстуру, которая пришла от вершинного шейдера.
Но в 2Д графике только плоскости, так что всё скатывается в примитивизм и работу с пиксельным шейдером. Я в теории не очень, так что могу здесь чепухи наговорить, но именно так я это понимаю.
Итак, создали шейдер и во второй вкладке нужно получить такое:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform vec3 blend_color; // Это параметр, который будем передавать в шейдер
//… сюда вставляем эту вот порятнку сверху
void main ()
{
vec4 col = v_vColour*texture2D (gm_BaseTexture, v_vTexcoord);
gl_FragColor = vec4(BlendOverlay (vec3(col),blend_color),col.a);
}
Для тех кто не в курсе как работает шейдер, поясняю. Вот рисуете вы спрайт с применением шейдера.
И код выше, то что внутри функции main отработает на КАЖДЫЙ ПИКСЕЛЬ вашего спрайта.
Шейдер ВСЕГДА обрабатывает ОДИН пиксель и ВСЕГДА возвращает ТОЛЬКО цвет с альфой. А то что мы в процессе знаем немного больше контекста о том какие текстуры томятся в видеопамяти как раз и помогает делать все эти крутые визуальные эффекты.
Собсно, такая структура обработки графики по пикселям и даёт буст на видеокартах. Ведь что такое видекарта? Это сотни простеньких процессоров, каждый из которых как раз обрабатывает один пиксель. И за счёт того, что одна текстура подаётся на эти процессоры параллельно — и достигается немыслемый буст производительности. Область применения правда весьма ограничена, однако умльцы умудряются на шейдерах мини-игры писать =)
В шейдерах работа идёт преимущественно с векторами (это типа мини-массивы на 2−4 элемента).
vec2 — это пара x и y, задаётся через функцию vec2(x,y)
vec3 — это тройка r,g,b, задаётся через функцию vec3(r,g,b)
vec4 — это четвёрка r,g,b,a, задаётся через функцию vec4(r,g,b,a)
Чтобы потом обратиться к конкретным значениям вектора можно писать вот так col.r, col.g, col.b — это к цветовым составляющим цвета. Но для них есть синонимы col.x, col.y, col.z — для работы с координатами. Это чисто для удобство чтения кода. Там есть и другие приколы типа можно взять две составляющие col.rg или col.xy — это уже будет vec2 которые мы на ходу сделали из vec3 или vec4.
v_vTexcoord — это vec2 — координаты пикселя внутри текстуры
v_vColour — это полные цветовые данные о пикселе в текстуре
Всё что отмечено ключевым словом uniform будет приезжать в шейдер в качестве параметра извне. В нашем случаем мы будем задавать цвет для наложения.
Первая строчка в функции main просто получает текущий пиксель в текстуре gm_BaseTexture в которую передаётся то что рисуем. Как раз тот самый спрайт.
Вторая строчка — это обычный return, но вот так вот хитро реализованный. По сути gl_FragColor — это итоговый цвет пикселя, который будем рисовать. А то что по факту мы сюда вернём пиксель, который на самом деле соседний пиксель, который мы нагло выдрали из текстуры — вообще никого не волнует.
Итого, зная цвет пикселя и его координаты, мы можем делать много разных крутых вещей типа сдвигов по текстуре, смешиваний
В примере выше я использую режим наложения overlay конкретного цвета, который передаётся в шейдер, таким образом можно подкрасить спрайт в нужный цвет (хотя лучше это делать режимом color)
Чтобы использовать другие режимы, нужно менять функцию BlendOverlay на соответствующую из того что предлагает шейдер. Можно сделать универсальный шейдер на все режимы смешивания. Для этого добавить ещё один параметр шейдера по которому выбирать нудную функцию. Тут вообще всё изи.
Как это применять конкретно в Game Maker Studio 2. Довольно просто, в событие Draw можно написать что-то такое:
var color = c_fuchsia;
shdrcolor=shader_get_uniform (shdr_blend_mode,"blend_color");
shader_set (shdr_blend_mode);
var arr;
arr[0]=color_get_red (color)/255;
arr[1]=color_get_green (color)/255;
arr[2]=color_get_blue (color)/255;
shader_set_uniform_f_array (shdrcolor,arr);
draw_sprite_ext (mysSprite,0,x,y,1,1,0,c_white,alpha);
shader_reset ();
Я дал своему шейдеру имя shdr_blend_mode, и использую это имя чтобы
- Включить шейдер функцией shader_set
- Взять id параметра blend_color при помощи функции shader_get_uniform, чтобы потом передать в него нужное значение
В шейдере параметр blend_color является vec3, значит передавать нужно массив для чего есть специальная функция shader_set_uniform_f_array. Таких функций есть несколько для разных типов параметров. Вектора передаются массивом, отдельные числа — своими функциями. Для текстур есть свои приколы, о которых ниже.
Цвет в Game Maker Studio 2 представляется числом из скольки-то там бит, по сути это тройка из значений от 0 до 255. Поэтому я делю каждую компоненту цвета чтобы получить значение от 0 до 1.
Всё что будет нарисовано между shader_set (…) и shader_reset () — уезжает на видеокарту где обрабатывается шейдером. Я к тому что это может быть не один спрайт, а много чего разного.
Итого взаимодействие GML-кода и кода шейдера можно представить вот так:
Судя по тому что я видел в сети, для других движков этот процесс не сильно отличается по идеологии. Функции только другие по названию да и всё.
Пример 2 — наложение слоёв
Второй пример — смешивание двух текстур, ради чего я как раз и писал этот пост. Цвета подмешивать хорошо, но хочется же прям наложения и тру-смешивания!
Теперь я хочу превратить серую и унылыую палитру в красочную и яркую, чтоб глаза моих дронов светились и красиво окрашивали стены в свой цвет.
Снова создаём шейдер. И в пиксельной части пишем:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform sampler2D lightmap;
//… портянка смешиваний для фотошопа
void main ()
{
vec4 col = v_vColour*texture2D (gm_BaseTexture, v_vTexcoord);
vec4 blend_color = texture2D (lightmap, v_vTexcoord);
gl_FragColor = vec4(BlendOverlay (vec3(col),vec3(blend_color)),1);
}
Тут вроде всё понятно. На смену blend_color пришёл параметр sampler2D, вместо одного цвета целая текстура с кучей таких цветов!
В функции main берём цвет с текстуры подобно тому как это делается с цветом того что мы рисуем.
Далее просто смешиваем два этих пикселя.
А вот применять этот шейдер стало чуток сложнее, ведь передать нужно целую текстуру. НЕ СПРАЙТ!
var bufferTex = surface_get_texture (buffer);
shader_set (shdr_overlay);
var t_sampler = shader_get_sampler_index (shdr_overlay, «lightmap»);
texture_set_stage (t_sampler, bufferTex);
draw_surface (mySurface,0,0);
shader_reset ();
Вот у меня есть сурфейс buffer в котором отрисован весь свет, который я хочу наложить на основное изображение, которое содержится в сурфейсе mySurface
Чтобы получить текстуру, нужно взять её явным образом функцией surface_get_texture. Текстуру можно получит и другими способами, например из спрайта функцией sprite_get_texture.
Вместо shader_get_uniform используется другая функция shader_get_sampler_index, которая делает то же самое, но для текстур. А для передачи текстуры в шейдер texture_set_stage.
Для наглядности снова приведу схему работы шейдера:
А в чём сложность тогда, если только слова разные? Здесь важно помнить что функция surface_get_texture возвращает ссылку на текстуру, которая жива пока жив сурфейс. То есть если сурфейс будет перерисовываться то содержимое текстуры так же перерисуются. И я уже напоролся на это — решил сэкономить на сурфейсах и стал сразу рисовать в тот же буфер из которого взял текстуру для наложения, и получил белый экран. Полчаса пытался понять в чём дело.
Второй подводный камень заключается в том, что иногда приходится накладывать текстуру с альфой и тогда наложение делается по-другому.
В шейдере вместо функции смешивания нужно использовать функцию смешивания с альфа-каналом, в которой как раз была допущена ошибка, вот правильная функция:
#define BlendOpacity (base, blend, F, O) (F (base, blend) * O + base * (1.0 — O))
//Ниже неправильный вариант из исходников
//#define BlendOpacity (base, blend, F, O) (F (base, blend) * O + blend * (1.0 — O))
А чтобы её использовать, нужно шейдер примерно так написать:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform sampler2D lightmap;
//… портянка смешиваний для фотошопа
void main ()
{
vec4 col = v_vColour*texture2D (gm_BaseTexture, v_vTexcoord);
vec4 blend_color = texture2D (lightmap, v_vTexcoord);
gl_FragColor = vec4(BlendOpacity (vec3(col), vec3(blend_color), BlendSoftLight, blend_color.a),1);
}
Но для того, чтобы у вас ОК смешалось нужно ещё и правильно подготовить сурфейс альфой, а именно — очистить альфа-канал:
draw_clear_alpha(c_black,0);
Пример 3 — адаптируем шейдер из интернета
Что делать если шейдеры хочется, но мозг слишком маленький для того чтобы выдумывать их самому?
Как у меня, например. Я нашёл для себя ShaderToy. Довольно большая база шейдеров с поиском по тегам. Можно знатно позалипать, это да…
Но вот беда в том, что просто так копипастить эти шейдеры в Game Maker Studio 2 не получится. Здесь наблюдается lack of tutorials =(
- Я так и не смог завести шейдеры с мультипассом/буферами. Просто не понимаю как это адаптировать. Там какие-то рекурсивные ссылки, которые я ХЗ как в Game Maker Studio 2 делать…
- А для одностраничных шейдеров нужно поприседать.
Приседание 1
Во вкладке Shader Inputs находятся те параметры, которые ShaderToy передаёт в шейдер автоматически, а для Game Maker Studio 2 придётся явно указывать эти параметры.
Но тут достаточно копипасты нужного параметра и потом передать его в шейдер как uniform.
Приседание 2
Немного отличается синтаксис главной функции:
void mainImage (out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord.xy / iResolution.xy;
fragColor = texture (iChannel0, uv);
}
В шейдерах Game Maker Studio 2 это выглядит так:
varying vec2 v_vTexcoord; // Это аналог fragCoord
varying vec4 v_vColour; // А здесь просто хранится инфа о текущем пикселе
void main ()
{
// функция для текстуры немного отличается
vec4 col = v_vColour*texture2D (gm_BaseTexture, v_vTexcoord);
gl_FragColor = col; // вместо fragColor используется встроенный параметр GMS2
}
В ShaderToy в качестве входной текстуры используюся только каналы 0−4, которые можно выбрать под кодом шейдера (там кстати не только текстуры, но и 3Д модели, видео, аудио). А в Game Maker Studio 2 — это либо встроенная переменная gm_BaseTexture, в которой то что рисуется при вызове шейдера. Либо любая другая текстура, которую передали в шейдер через параметр sampler2D (про это было написано выше)
Я люблю схемы, так что снова для наглядности:
Приседание 3
Есть некая чехарда с координатами текстуры. В ShaderToy в параметр fragCoord прилетает координата пикселя на экране. В то время как функция взятия пикселя с текстуры работает с координатами от 0 до 1. Если мы знаем ширину и высоту экрана, то легко можем пересчитать координаты из пикселей в 0.1, поэтому в примерах на ShaderToy почти везде встречается вот такая строчка:
vec2 uv = fragCoord/iResolution.xy;
Вообще деление на iResolution можно встретить часто в примерах на ShaderToy.
А в шейдерах Game Maker Studio 2 параметр v_vTexcoord уже в нужном виде с координатами от 0 до 1. Вот и получается несоответствие. Так что для особо сложных случаев придётся делать финт ушами — явно передавать в шейдер iResolution через параметр uniform. И так же передавать пиксельные координаты, которые К СЧАСТЬЮ можно взять из вершинной составляющей шейдера. Для этого нужно вернуться в первую вкладку шейдера и узреть что-то такое:
attribute vec3 in_Position; // (x,y,z)
//attribute vec3 in_Normal; // (x,y,z) unused in this shader.
attribute vec4 in_Colour; // (r,g,b,a)
attribute vec2 in_TextureCoord; // (u,v)
Вот нас интересует первый параметр, который в пиксельную обработку шейдера по-умолчанию не передаётся. Добавим это — тогда вершинный шейдер из дефолтного превращается в нужный добавлением всего двух строчек:
attribute vec3 in_Position; // (x,y,z)
//attribute vec3 in_Normal; // (x,y,z) unused in this shader.
attribute vec4 in_Colour; // (r,g,b,a)
attribute vec2 in_TextureCoord; // (u,v)
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec3 v_vPosition; // <--------------Вот эту строчку добавить
void main ()
{
vec4 object_space_pos = vec4(in_Position.x, in_Position.y, in_Position.z, 1.0);
gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
v_vColour = in_Colour;
v_vTexcoord = in_TextureCoord;
v_vPosition = in_Position; // <--------------Вот эту строчку добавить
}
А теперь просто в начале пиксельной обработки шейдера добавить строчку:
varying vec3 v_vPosition;
Краткая схема того что сделали:
Таким образом, мы связали вершинный шейдер с пиксельным, и теперь вместо fragCoord от ShaderToy используем v_vPosition, а параметр iResolution передаём как uniform вручную.
Ниже небольшой пример по адаптации шейдера подсветки линий.
attribute vec3 in_Position; // (x,y,z)
//attribute vec3 in_Normal; // (x,y,z) unused in this shader.
attribute vec4 in_Colour; // (r,g,b,a)
attribute vec2 in_TextureCoord; // (u,v)
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec3 v_vPosition;
void main ()
{
vec4 object_space_pos = vec4(in_Position.x, in_Position.y, in_Position.z, 1.0);
gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
v_vColour = in_Colour;
v_vTexcoord = in_TextureCoord;
v_vPosition = in_Position;
}
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec3 v_vPosition;
uniform vec2 iResolution;
#define AMPLIFER 2.0
vec4 getPixel (in int x, in int y, in vec2 fragCoord)
{
return v_vColour * texture2D (gm_BaseTexture, (fragCoord.xy + vec2(x, y)) / iResolution.xy);
}
void main ()
{
vec4 sum = abs (getPixel (0, 1, v_vPosition.xy) — getPixel (0, -1, v_vPosition.xy));
sum += abs (getPixel (1, 0, v_vPosition.xy) — getPixel (-1, 0, v_vPosition.xy));
sum /= 2.0;
vec4 color = getPixel (0, 0, v_vPosition.xy);
color.r += length (sum) * AMPLIFER;
gl_FragColor = color;
}
Вот и всё
Вроде не так уж и страшно это всё было. Удачного шейдинга!
- 04 февраля 2020, 02:11
- 023
у тя на картинке где "пример 2" красный цвет создает грязь при затухании, потому что темнеет, а не слабеет. может какую нить отсечку допилить по значению?
Господи, как вовремя! Только вчера мучилась с image_blend и поняла, что он работает по-своему и не так как мне надо.
Добавила пост в избранное.
Хорошая тема, как раз изучаю Game Maker сейчас!
Погодь-погодь! А разве это не цвет которым рисуется спрайт/текстура?
Цветовые данные пикселей мы берем вот здесь (gm_BaseTexture - текстура, v_vTexcoord - координаты пикселя в текстуре):
Тем более в след. абзаце ты об этом и пишешь.
Опечатка:
Вот этот момент я не просёк. Хочешь сказать, что это тот цвет что устанавливается в draw_set_color ? Или тот цвет с которым блендидтся спрайт/сурфейс в _ext функциях? Это нужно будет прочекать.
Скорее всего да. В Юнити и Love2D так. В Юнити туда попадает основной цвет материала, в Love2D - текущий цвет рисования, заданный функцией love.graphics.setColor.
В шейдер кроме текстуры (tex), текстурных координат (tc) и экранных координат (sc) передается еще основной цвет (baseColor). При отрисовке сложных материалов шейдеру может передаваться несколько цветов, например основной цвет и цвет отражения. Тогда доп. цвета нужно передавать явно, самому.
А вообще большое тебе спасибо, избавил от кучи работы.
Теперь подробный туториал по шейдерам можно не делать.
Хотя я бы описал все более подробно и разбил статью на части.
Потестил, нет. Туда попадает тот цвет, который указан в blend для ext функций, типа draw_sprite_ext и draw_sufrace_ext. Это точно. Но вполне может быть что для HTML5 платформы туда будет попадать как раз draw_set_color, потому что для HTML5 эта функция устанавливает цвет смешивания для обычной отрисовки спрайтов.
В любом случае, vColour - это не данные от цвете пикселя в текстуре, надо исправить.
Господа, по моему вы оба ошибаетесь. Ваш v_vColor просто игнорируется и все, также как и in_Color. Откуда данные в них? Вы даже сами не понимаете, данные какого источника в нем должны быть, т.к. эти переменные просто объявляютя и все. Смотреть здесь похожий пример. Либо я ничего не шарю в шейдере, либо GMS это та еще жопа и нужно быть предсказателем.
В вершинной части должна по сути быть вот такая херабора:
Вообще нет. Я же написал выше что потестил. Он передаётся через использование
в качестве аргумента цвета смешивания colour/col.
GML - это скриптовый язык, а трансляция переменных в шейдер остаётся за кадром. Конечно было бы здорово в справке этот процесс более подробно описать по тому какие переменные доступны, но она пока что очень скудна. Даже в официальном туториале из 4-х частей ничего про это нет. Честно говоря, этот туториал - хуета, в которой ничего не понятно.
Что-то как то совсем не явно передаётся. Мне не понятно. Совсем, совсем. Шейдер в GMS это сложно. Не вижу общей картины. Вот например, в Годот есть shader_material("color", color). Я понимаю, что шейдеру отдал цвет. А в статье нет самой передачи или я не увидел.
Статья говно. Многое по этой теме познаётся только личным опытом. На самом деле работа с рисованием в GameMakerStudio2 так себе сделана. С альфа-каналами срань полная, с шейдерами много непоняток - сделано не как у всех.
Основная проблема в том, что во вторую студию завезли довольно много фич, но нет норм документации на тему того как эти фичи использовать =(
Но по удобству разработки ИМХО, GMS2 всё равно лучший инструмент. В других может быть конкретно это сделано лучше, но по общему пайплайну я пока не видел более годного решения. Я лучше подзадрачу именно эти места практикой чем буду ходить и ныть на тему того как всё по-уродски сделано.
Первый раз слышу про эту статью, чтоб далеко не ходить - вот пример сразу из справки:
shader_set(shader_Glass);
shader_params = shader_get_uniform(shader_glass, "u_vParams");
shader_set_uniform_f(shader_params, 0.1, 0.8, 0.25);
Думаю, так же можно сделать и с цветом.
Это понятно что можно. Но вопрос изначальнй в том что попадает в in_Color и v_vColor и что про это в справке ни слова. Хотя это всё в дефолтном шейдере в GMS2.
Да, справка Гамака это беда. Из-за которой всякие недалёкие люди считают что Гамак в принципе отстой. Объективно говоря, из-за справки это в итоге становится правдой. Эх.
С другой стороны, чтобы просто сделать игру как тот же Локомалито или Йео, не обязательно знать ни одного шейдера и прочих понтов просто для графики. Этот момент почему-то все упускают регулярно. В твоём случае это хотя бы обосновано.
а есть на этом движке современные визуально и красивые игры вообще, кстате? пускай даже это будет платформер, как ори или там халлов кнайт
Hyperlight drifter
Katana Zero
The Swords Of Ditto
Blazing Chrome
LevelHead
Deaths Gambit
Nuclear Throne
Эти пойдут?
я не вижу в них внятного освещения, эмиссив материалов, партиклей на тыщуединиц с физическим взаимодействием
вот годреи в катсцене в катане, показанные явно как крутое достижение; полное отсутствие теней в гиперлайте, хотя казалось ба 3D - источник освещения поставь, тени сами появятся; только дитто и радует всякими глоу. кажется, что вся работа на художниках, а не на движке. не знаю насколько это современно. 2010?
(Blazing Chrome, LevelHead, Nuclear Throne смешно, конечно)
Делать фоторелизистичные игры с тотально проёбанной физикой. не знаю насколько это современно. 2000?
У каждого движка свои грехи =)
И если говорить про современность, то чем отличватеся игра, допустим 2015 года от игры 2020? Я вот разницы не вижу никакой, никакого прорыва в технологиях. Всё ровно то же самое.
не знаю про сравнение с 2015м, мне сложно сравнить прям попунктно. но вижу что технологии Trine из 2009-го многим отчего-то будто вообще неизвестны.
Так чтобы Hyperlight drifter мог конкурировать с dungeon of endless по качеству динамической картинки достигаемой легкодоступными способами, а Hero Siege если не с Hades то хоть с Heroes of Hammerwatch
Ты серьёзно считаешь эту игру эталоном картинки?
В Дрифтере всё норм с динамикой и картинкой.
не эталоном совсем, а использованием технологий которые пошли бы на пользу дрифтеру. а щас я не различаю в нем вообще ничего, пол от пропасти, пули и нпс от окружения, врагов от ui.
то есть я понимаю что читаемость картинки это больше дизайнерский скилл, но элементарные свечения, динамические трейлы, глубина освещения, тени и разные свойства у поверхностей это те элементы визуального языка игры, без которых в дрифтера у меня просто не получается играть.
И это обидно)
Определить бы, о чём речь идёт. "Эмиссив материалы" мне ни о чём не говорит, но и не является показателем визуальной красоты в моём понимании. Я бы назвал красивым такое, например:
https://www.yoyogames.com/showcase/285/the-eternal-castle-remastered
В любом случае, вот шоукейс, там ещё есть кнопка VIEW MORE внизу. Если всё это не нравится, значит нет. Правда, и не значит, что это когда-либо было важно.
https://www.yoyogames.com/showcase
излучающие, как раз то что хейз сейчас делает, с возможностью сделать засветку на особо ярких местах
для игры как воплощения гейммеханик, разумеется неважно. А вот важно ли для инструмента по созданию видеоразвлечений, ну, вам виднее
Тот же Hyperlight Drifter вполне круто выглядит визуально. На самом деле нет гарантий, что на гамаке нельзя сделать что-то визуально крутое со всеми этими свистоперделками. В GMS2 этих эффектов нет из коробки в отличие от других движков (юнити к примеру). Но как показала лично моя практика - и динамический свет и крутые световые шейдеры - это всё возможно, если с этим прям разбираться. GMS2 часто воспринимают как "детский конструктор", т.е. ЦА как раз люди которые не особо хотят с этим разбираться. Поэтому "тру-авторы" идут в другие движки. Ну там они получают что хотят вместе со своими проблемами.
Но вообще неважно кто чё делает, главное чтоб игры хорошие получались. GMS2 в шейдеры могёт. Вот это важно =)
Если эффект на скриншоте сделан на шейдерах, то его можно просто оттуда вынуть и адаптировать, а вот если нет... то нет, спасибо, Кэп. Не знал что там такое встроено.
Вот, например стандартный пиксельный шейдер Love2D (когда не выбран вообще никакой шейдер).
Как видишь, здесь фигурирует цвет (color).
Это текущий выбранный цвет, которым ты рисуешь спрайт, тут он умножается на цвет пикселей в текстуре. Если цвет черный - весь спрайт будет черный. Если цвет будет белый - спрайт будет точно такой же, как в текстуре, без окрашивания.
Вообще я за основательный подход в обучении, с разжовыванием всего и вся.
Проблема в том что большинство не хочет учиться и разбираться основательно, вникать в длинные талмуды и разбираться им бы решить одну конкретную проблему и все.
А те кому надо - не могут найти нужную себе информацию из-за огромного количества поверхностных туториалов в сети, которые учат только чему-то конкретному.
Со статьями беда. Конкретно по Game Maker Studio 2 есть только пара официальных туториалов в которых ничего не понятно. А смысл этого в чём, если прочитав статью я всё равно не смогу шейдеры применять?
Я азы шейдеров постигал по какой-то статье о шейдерах в shadertoy, потом пытался прикрутить себе, задавал вопросы на офф форуме YoYo чтоб понять что чему соответствует.
И основная проблема шейдеров Game Maker Studio2 в том что их днём с огнём не сыщешь. Ни в сети, ни на форумах, ни в ассетсторе того же гамака. Точнее там есть базовые шейдеры типа колоризации, волн, пикселизации и т.д. А именно той крутизны, которую можно найти на том же ShaderToy - такого нету.
И мне кажется, я в статье достаточно подводных камней осветил чтобы прочитав статью можно было использовать готовые шейдеры и разбираться по мере интереса и желания. А так да, можно было бы ещё глубже в синтаксис шейдеров закопаться, но смысла пока в этом не вижу для себя.
Никакой опечатки нет - шейдеры это капец нудно. :yak: Хейз нехило заморочился чтоб с ними разобраться.
Чтобы не было недопонимания, добавлю, что очень уважаю учёбу и упорный труд, за проделанную работу - респект.
Просто содержание поста не только language-, но ещё и developer-specific.
Это не совсем так. Вершинный шейдер принимает координаты вершин, будь то 3D форма или обычная текстура. Не происходит превращение векторного в пиксельное - это немного неправильное определение. В вершинном шейдере можно определять расположение текстуры на объекте текстурирования, проводить различные трансформации (например, спрайт травы покачивается и всячески извевается в такт ветра или эффект морской глади / поверхности воды).
Сам болею копипастой... здесь в примере у тебя отпала "col", видимо она должна именоваться как out_color, иначе возникает диссонанс.
Ещё небольшая заметка, если позволите. Uniforms - это унифицированные глобальные переменные, которые доступны шейдеру в любой момент времени из любого места. Удобны для связки фрагментного и вершинного шейдеров.
Точно, поправил.
Для связки вершинного и пиксельного вроде как vraying используется, а через uniforms передаются параметры из кода игры в код шейдера. Разве нет? По крайней мере в GMS2 это так работает.
Да, uniforms именно так и работают, но по всем учебникам - это просто глобальные переменные доступные в любой момент времени, и даже пишут, что они read-only, но на практике это не так, uniforms могут меняться в шейдере. Да, varyings именно для этого и служат, но в Godot я редко где встречал его применение, в основном uniforms.
Не читал, но одобряю.
Статья про ШейДыры очень познавательная. Сам я шейдеры не очень активно использовал. Просто пока не было необходимости. В основном брал готовые из примеров и слегка модифицировал. В DirectX SDK очень много рабочих примеров по использованию и тестированию шейдеров. Но да, тема специфичная и инфы по ней не очень много. Поэтому любая инфа только на пользу. Еще раз респект за статью ! Считаю будет полезна как новичкам, так и тем кто уже немного знаком с темой и хочет вникнуть больше.