Как правильно кушать шейдеры в Game Maker Studio 2 и не подавиться

Речь пойдёт в том числе и о проблеме, с которой я столкнулся в процессе адаптации моей игры Sig.NULL под FullHD разрешение для релиза на XBOX. Я решил переделать графику под современные реалии, чтоб не так позорно и стыдно было.

T8v0pf3
Картинка для понта — WIP на момент написания поста

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

Я раньше считал что шейдеры — это кусачая тема, где без знания матанализа и линейной алгебры никуда.

Image result for шейдеры мемы"

Но нет, нифига. Простые вещи в духе перекраски и обесцвечивания можно сделать своими силами, если понимать как работает rgb палитра и знать что такое координатная плоскость.

Здесь практически не будет картинок, в основном текст и код — для тех кто хочет как и я погрузиться в мир шейдеров используя Game Maker Studio 2, но уже по протоптанной мной дорожке. Пользователям других движков тоже может быть полезно, но шейдеры — это такая штука language specific, как говорится. То есть в разных средах разработки может применяться по-разному.

В последнее время я часто графику прототипирую в фотошопе, и часто использую режимы смешивания чтобы достичь тех или иных результатов. Бывает так, что штатные режимы смешивания в движке грешат. Ну вот есть допустим режим смешивания overlay, в том же Game Maker Studio 2 можно добиться ПРИМЕРНО того же самого через gpu_set_blend_mode.

PIQEqCq
Хочется чтобы глаза у дронов не проваливались в ебеня

Примерно… ПРИМЕРНО!

Но не совсем того. Например, белые пиксели он замешивает с цветом оверлея, а в фотошопе они остаются белыми.

Я покопался в инете и нашёл 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 — подкрашивание

Начну с примера попроще. Допустим, нужно наложить какой-то цвет поверх спрайта с режимом смешивания. У моих дронов в игре есть «глаза», которые я хочу красить в цвет дрона, посмотрим как это делается.

jiUIFsx
Снова какая-то херота с глазами несчастных дронов

Есть в Game Maker Studio 2 встроенное смешивание с цветом, которое мой прекрасный глаз дрона целиком заливает цветом. А я хочу чтобы блик остался! На помощь придёт шейдер!

Сперва нужно создать дефолтный шейдер (он по стандарту GLSL ES если что)

ESkZqS7

Открывается две вкладки:

  • вершинный шейдер (о котором будет в третьем примере) — занимается пикселизацией, то есть векторное превращает в пиксельное. Например, 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-кода и кода шейдера можно представить вот так:

2aFOVeQ

Судя по тому что я видел в сети, для других движков этот процесс не сильно отличается по идеологии. Функции только другие по названию да и всё.


Пример 2 — наложение слоёв

Второй пример — смешивание двух текстур, ради чего я как раз и писал этот пост. Цвета подмешивать хорошо, но хочется же прям наложения и тру-смешивания!

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

t9oItIG
Хочу вот так вот свет наложить

Снова создаём шейдер. И в пиксельной части пишем:

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.

Для наглядности снова приведу схему работы шейдера:

ZrdmvL6

А в чём сложность тогда, если только слова разные? Здесь важно помнить что функция surface_get_texture возвращает ссылку на текстуру, которая жива пока жив сурфейс. То есть  если сурфейс будет перерисовываться то содержимое текстуры так же перерисуются. И я уже напоролся на это — решил сэкономить на сурфейсах и стал сразу рисовать в тот же буфер из которого взял текстуру для наложения, и получил белый экран. Полчаса пытался понять в чём дело.

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

ElvGAhR
Хочу теперь не просто освещение наложить, а качественно край затемнить!

В шейдере вместо функции смешивания нужно использовать функцию смешивания с альфа-каналом, в которой как раз была допущена ошибка, вот правильная функция:

#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 придётся явно указывать эти параметры.

vgNiHbe

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

Я люблю схемы, так что снова для наглядности:

lCPXeMs

Приседание 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;

Краткая схема того что сделали:

rYYWRAU

Таким образом, мы связали вершинный шейдер с пиксельным, и теперь вместо fragCoord от ShaderToy используем v_vPosition, а параметр iResolution передаём как uniform вручную.

Ниже небольшой пример по адаптации шейдера подсветки линий.

Исходник на ShaderToy

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

Вот и всё

Вроде не так уж и страшно это всё было. Удачного шейдинга!

P. S. И не дай бог в шейдере вы присвоите float переменной число без decimal =)

Blank