Xor Shaders
  • Tutorials
  • About

#2 Black and white shader

12/5/2014

32 Comments

 
In this tutorial, you will make your first black and white shader.
Make sure you have seen the SHADER BASICS tutorial because we will be building off of things we have already learned.

vertex to fragment

You probably noticed "varying vec2 v_vTexcoord; " in the  fragment code. This is a 2 dimension vector from the vertex shader (vec2). This tells the pixel coordinates for each pixel (which I will explain in the SAMPLERS AND TEXTURES section). You should also see "varying vec4 v_vColour;". This is for when you set image_blend on a sprite or draw_vertex_colour in primatives. So if you remove "v_vColour", then changing image_blend on a sprite would make no difference in the shader. We won't really need to mess with these for now.

More components

You know the components R, G, B, and A, and you can use these for any vector, but you should also know that there is X, Y, Z, and W. These are used for positioning variables in general while R, G, B, and A are used for color vectors, so you could still use R and G for texture coordinates, but it just makes more sense to use X and Y.

samplers and textures

Samplers are basically just textures. There are many types of samplers (cube map, 3d, or 1d samplers) but GameMaker: Studio only supports 2D samplers at this point. The first sampler you need to know is "gm_BaseTexture". It is the texture that you applied the shader to. So if I applied the shader to a sprite, then "gm_BaseTexture" is that sprite. You can even apply the shader to a group of drawn sprites in which case "gm_BaseTexture" is the whole group of sprites. In most cases you'll want the shader to be applied to textures that are in power of 2 sizes (like 256 by 256 or 512 by 512),  because those the sizes are easier for the shader to work with (it can create distortions at other sizes). The function "texture2D()" uses two arguments. The first argument is for the sampler (we'll use "gm_BaseTexture" for now).  The second argument is a vec2 (for the position on the texture to get). Texture positions are between 0 and 1, so "texture2D( gm_BaseTexture,vec2(0.5,1.0))" would return the red, green, blue, and alpha values at the bottom middle pixel of the texture. We'll leave the coordinates at "v_vTexcoord" because we are not modifying the shape or position of the texture. Instead we are modifying the color results of the texture to get shades of gray. We'll get the average of red, green, and blue values. This makes the colors only gray. The code should look like this: 

varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main()
{
    vec4 Color = texture2D( gm_BaseTexture, v_vTexcoord );
    gl_FragColor = vec4( vec3(Color.r+Color.g+Color.b)/3.0, Color.a);
}



So what this code does is it sets "Color" to the texture color. Then gl_FragColor is set so that the red, green and blue values are averaged while the alpha is still set to the textures alpha.
The result should look like this:
Picture
On the left is the original texture and on the right is the texture processed through the shader.
This black and white shader is alright, but not very precise. That is because some colors are brighter than the average. To get the proper values we'll try this:

varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main()
{
    vec4 Color = texture2D( gm_BaseTexture, v_vTexcoord );
    vec3 lum = vec3(0.299, 0.587, 0.114);
    gl_FragColor = vec4( vec3(dot( Color.rgb, lum)), Color.a);
}



This is the result:
Picture
On the left is the our first black and white shader and on the right is the new one.
The function "dot()" gets the dot product of two vectors. The reason we do this is because some colors are brighter then others (green is the brightess while blue is the darkest). The dot product makes sure the green is brighter then red and red is brighter then blue. So it basically is a short way to type:

gl_FragColor = vec4( vec3( Color.r * lum.r+Color.g * lum.g+Color.b * lum.b), Color.a);

That fixes the brightness.
DOWNLOAD EXAMPLE
32 Comments
Fe
12/16/2014 05:00:15 am

I'm confused about how you set vertices.

vec4( vec3(Color.r+Color.g+Color.b)/3.0, Color.a);

Doesn't vec4 require 4 values? You put a vec3 there, which I guess means it'll be understood as 3 different values by GLSL ES shaders.

But you divided that vertex by 3. It worked but to me it made no sense. How can you divide a vertex by 3? Worse, is that inside vec3 you added only one value (the sum of color's rgb values).

Why can vec3 accept just 1 value but vec4 can't accept anything but 4 values? How can vec3 be just one number and then you put that number on vec4 and it accepts it just fine.

Even more confusing to me was that you did it like this
vec3(r+g+b)/3

instead of this

vec3((r+g+b)/3).

I thought I had understood what a vertex was from the first tutorial, but now I don't know how they work anymore. I thought a vertex was somehow like an array, but this is very odd behavior for one.

As you can see I'm extremely confused by that.

Reply
Xor
12/17/2014 12:03:29 am

You can set any vectors with smaller vectors as long as you have the right number of values. when I did vec3((r+g+b)/3) it is the same as vec3( (r+g+b)/3), (r+g+b)/3), (r+g+b)/3)) because when you put one value in any vector it sets all the components to that value. So with a vec3 I can either put one value in or three values in. I could even put a vec2 like this: vec3 vector = vec3( vec2(1.0), 0.0);
What this does, is it sets it to this: vec3( 1.0, 1.0, 0.0).
The reason I divide the vector by 3 is because I want to average the red, green, and blue values. I hope this clears it up some.

Reply
Fe
12/17/2014 01:04:04 am

It does! Thanks. Hey also I'm going to post some questions in that last tutorial of yours... just letting you know :).

It's so hard finding stuff explaining shaders for GML. I mean I know there are GLSL shader tutorials already but it seems to me lots of stuff are very specific to gamemaker, like how it has some attributes set up from the start, etc.

If you have more resources to point me to it'd be great.

fe
12/16/2014 05:15:51 am

Oh also, this
void main()
{
vec4 Color = texture2D( gm_BaseTexture, v_vTexcoord );
vec3 lum = vec3(0.299, 0.587, 0.114);
gl_FragColor = vec4( dot( Color.rgb, lum), Color.a);
}

Isn't working.

I get a syntax error at that last line.

But when I change it to this:
gl_FragColor = vec4( vec3(dot( Color.rgb, lum)), Color.a);

it works.

Reply
Xor
12/17/2014 12:04:43 am

It works on my machine. I'll change it to avoid errors though.

Reply
Jesse
2/23/2015 04:49:26 pm

""
Texture positions are between 0 and 1, so "texture2D( gm_BaseTexture,vec2(0.5,1.0))" would return the red, green, blue, and alpha values at the top middle pixel of the texture.
""
I think (0.5,1.0) would actually be the bottom middle pixel.

Reply
Xor link
2/25/2015 03:17:33 am

Thanks for pointing that out. It should be fixed now.

Reply
Alex
6/12/2015 10:42:58 pm

Question, where did you find the relative luminosity magic numbers that you used?

Reply
Xor
6/13/2015 01:39:04 am

A couple years ago I first began learning shaders. On the GameMaker Community. I posted a shader which simply averaged the red, green and blue. It was actually another Community user which shared the correct values for the components, and ever since then, those are the numbers I use.

Reply
Rianon
1/24/2017 07:43:16 am

Finally, I found this magic numbers. :)
https://en.wikipedia.org/wiki/HSL_and_HSV
Lighness section. It's about "luma", the weighted average of gamma-corrected R, G, and B, based on their contribution to perceived luminance.

Reply
Xor link
1/24/2017 08:56:03 am

Thanks for sharing!

Mornedil
6/23/2015 06:22:31 am

Your download links are broken.

Reply
Xor
6/23/2015 11:49:26 pm

Thanks for telling. The link should be fixed.

Reply
Milios
6/24/2015 08:03:10 am

gl_FragColor = vec4((dot(Color.rgb, lum), Color.a);

Should be

gl_FragColor = vec4(vec3(dot(Color.rgb, lum), Color.a);

Reply
Xor
6/25/2015 10:34:17 pm

I meant to change that already. It looks like it updated properly this time.

Reply
Anonymous
8/3/2015 12:58:42 am

Gradient Greyscale where factor determines how colorful to shade
0.0=grey
1.0=no greyscale applied

varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main()
{
vec4 Color = texture2D( gm_BaseTexture, v_vTexcoord );
float factor=0.0;
vec3 lum = vec3(0.299, 0.587, 0.114);
gl_FragColor = vec4(vec3(dot(Color.rgb,lum)*(1.0-factor)+(Color.rgb*factor)),Color.a);
}

Reply
Xor
8/6/2015 08:15:46 am

Interesting. You could also do it this way with the mix function:

gl_FragColor = vec4(mix(vec3(dot(Color.rgb,lum)),Color.rgb,factor)Color.a);

Reply
Nesrocks link
1/16/2017 11:04:51 am

I have a question: how can "Color.r+Color.g+Color.b" be a vec3? Shouldn't that result in a single float value instead of vec3?

Reply
Nesrocks link
1/16/2017 01:00:05 pm

I see that the same question was already asked. The answer is what I suspected it to be, but it's settled now.

Reply
LainV0hnDyrec
2/5/2017 01:49:41 am

Hi Xor, im kinda late in this tutorial but if ever you see this pls answer my question.

are the RGB float value 1.0 is almost the same as 255 RGB byte value?

if so, 0.5 RGB float value is the same as 255/2 RGB value right?

also in your example, the vec3 lum values, i tried to convert it to hex using the comparison i asked and the outcome was a dark green colour. so you use this as a basis so other colour wont outshine the others right? but my question is why use vec3 lum = vec3(0.299, 0.587, 0.114) as a basis?

Reply
Xor link
2/6/2017 02:58:01 pm

Hey there! To the first question, yes. In shaders, what is normally 0-1 is 0-255 in other applications such as make_color_rgb() in GML.
The luminance vector values can be found here: https://en.wikipedia.org/wiki/HSL_and_HSV#Lightness.

Reply
LainVohnDyrec
2/6/2017 04:01:19 pm

thnx Xor now im getting a hang of it and im still reading your tutorial and it was a God sent that this tutorials exists

btw Xor i encountered a problem with shaders, i hope you could identify the problem.

well the problem is that all my sprites and font that i applied a shader on it turns black in Android devices but works on windows target.

i tried some of your codes as well as my codes. even a shader script without any codes/changes on it, they work on Windows but still goes completly black on my Android Phones.

here are some additional info:
1. all of these sprites are grouped in seperate texture pages(some arein the"Monster Texture page" and "Font Texture Page").
2. i didnt change the vertex shader only the fragment shader, also didnt change the drop down option in shader (GLES something)
3. im using 1.14.15++ version, i failed to make my SDK work in the new updates
4. imnot using any surfaces, just the default one and set to keep aspect ratio.
5. im using a Samsung 7 and a Local Phone Cherry that almost same specs as my samsung.
6. i tried on forums, tech support and reddit no one can identify the problem.

hope you couldgive me am insight on whats happening.
even if this is a GM problem amd there are no work arounds ill still keep on reading you tutorisls.

Ale
7/8/2017 02:49:22 pm

Hiu,
it's possible to apply the shader to ALL the object in the room, like bg and object too?

Reply
LainVohnDyrec
7/8/2017 04:38:57 pm

yes it is. by setting shader in to the application surface.

learn first how Surfaces works and you can do this

Reply
Xor link
7/9/2017 05:24:33 am

Yes. Do what LainVohnDyrec said and use the application surface.
Try code like this in the Draw GUI event:
shader_set(shdr_black);
draw_surface(application_surface,0,0);
shader_reset();

Reply
Ben
8/7/2017 06:24:45 pm

But it breaks the code that enables and disables the shader when you press the Space button. :(

Xor link
8/8/2017 08:45:54 am

This is very easy to fix. Just check if the enabled variable is true before you set the shader. Like this:
if enabled //Only apply shader if the enabled variable is true.
{
shader_set(shader);
}

And in the space key pressed event put something like this:
enabled = !enabled;//If true this is set to false and vice versa.

Ale
7/9/2017 06:09:25 am

Something like this? (but it doesn't work)

surfa = surface_create(1280,720);
surface_set_target(surfa);
shader_set(shader);
draw_surface(application_surface,0,0);
shader_reset();
surface_reset_target()

Reply
Xor link
7/9/2017 06:44:20 am

Try the code I posted above. You shouldn't have to create a surface because the application surface exists by default and an other surface would be redundant. Your code should have still worked as long as you draw the surface though:
draw_surface(surfa,0,0);

Reply
Xor link
7/9/2017 06:47:11 am

Also, make sure you check if the surface does not exist (using surface_exists()) before creating it. Otherwise you will have a memory leak since it will be creating surfaces every frame and never deleting them.

Reply
Ale
7/9/2017 07:09:08 am

Hi again and thaks for your hint.
Just try the code that you suggest, but I have a strange problem

If I run the game, it will show only the background (in color way not in grayscale) and nothing else, no sprite or object!

Ale
7/9/2017 07:16:45 am

Ok fixed he problem.
I change from DRAW to DRAU GUI event the code and it works fine :)

Reply



Leave a Reply.

    Tutorials

    New tutorial at GMshaders.com!

    Categories

    All
    3D
    Beginner
    Intermediate

    Archives

    October 2021
    November 2019
    January 2019
    May 2016
    August 2015
    June 2015
    March 2015
    February 2015
    January 2015
    December 2014

    RSS Feed

Powered by Create your own unique website with customizable templates.