Shaders for beginners

I had many problems to understand shaders, but after getting a grasp - it is so easy, convenient and powerful! I present to you my twisted version of understanding shaders, hoping it will help other aspiring devs :wink: :man_teacher:

This is my slow path from a total noob to a less-noob-ish shader programmer :smiley: It is meant for someone totally new, so donā€™t mind a plain style and many simplifications or depictions (not exactly totally true, because saying them totally true demands more explanations and those explanations demands more exaplanations and so on, and so on :wink: ) Later things will be clear for you and you will get to know easily how it is really working.

The post is full of useful links and you should check them all - additionally I am encouraging you to check out the whole awesome Defold documentation after reading this post. :bookmark:


:paintbrush: Drawing a picture.

You use sprites to see a visual in your games. Defold uses (Nerdā€™s babble alert! :rotating_light:) OpenGL (a framework to draw graphics) to draw your sprite to a screen. There is a special way of telling the graphics card how it should be drawn. There are two programs that runs for every pixel every frame before the final color of that particular pixel is displayed on the screen.
Those are (OMG more babble! :rotating_light:) vertex program and fragment program. There are joined together in something called a material.


:womans_clothes: Materials to study

In Poland we say ā€œHow a horse looks like? Everybody see!ā€ (a not-so-scientific explanation of Mr. Chmielowski in his New Athens (encyclopedia) :smiley: )
Material is like it looks like - in our real world we have materials like silk :dress: , wool :sheep: or cotton :ear_of_rice:. They have, letā€™s think, a texture and a colour! (pun intended, you will get it soon :smiley: ). In computerā€™s world it specifies how a sprite/particle/mesh and other visual assets are rendered (displayed on a screen) to resemble something :desktop_computer: .


:receipt: Recipe for an image

This is a recipe (from here):

I will refer to it many times below:

In a 3D engine (like Defold is) everything visual is a bunch of points (vertices). They are transformed from 3D space to a 2D screen space like a camera captures a 2D image of a real 3D world. From those points we can specify some basic geometrical shapes (primitives), imagine retro style 3D:


(source: https://wallpapersden.com/retro-wave-wallpaper/2560x1440/)

But a screen is consisting only of pixels, and each of them can glow in one of different colors for us, so we can see a picture in whole. So we are taking picture of primitives we have (continuous geometry) and chop it into a handful of small pieces :hocho: (aka rasterize it into fragments). Then, we have an interpolated colour of a fragment (and we can modify it), so we can finally light that one diode in our newest OLED monitor. And another diode. And anotherā€¦


:triangular_ruler: Vertex program

It takes care of the first part of that process - calculating position of vertices (aka points in a 3D space that specify the geometry) from 3D space to a 2D screen space - so it somehow answers a question - where to draw. But letā€™s leave it for now - it demands some further explanations about cameras, projections and so on and you will need it mainly for 3D games. For now we are taking into account sprites in 2D games and spriteā€™s geometry is simple as it is - a rectangle (you will see it too). Weā€™ll use a default vertex program.


:traffic_light: Fragment program

It simply defines what color should have each fragment (pixel). Itā€™s used in the last stage of the process of preparation of your image to be displayed on the screen. So it somehow answers a question what to draw. Thatā€™s it, canā€™t say here anything more useful or wiser :mage: , but I want that definition to be a little bit longer so it looks professional :smiley:

After reading above and getting a grasp of what it is, you should read about those programs in manuals for a deeper understanding.


:muscle: Excersises

You must try something in order to stop being afraid of that thing. You must face that monster before it hits you. But when it hits you, you will probably blink in white (games are teaching!)


(https://sventhole.itch.io/blademaster)

We will make that effect in shaders, but normally it will be more efficient to just tell your graphic designer (or yourself) to merely draw that one frame in white, ok? Although such exercise will teach you:

  1. getting data from a texture
  2. changing color of pixels
  3. sending data to a shader

:checkered_flag: Letā€™s start!

Create an empty project in Defold with a sprite.
Add an image - make an atlas and add that image into it. Add game object with sprite to your collection and set sprite image and a default animation to your new image. Do I need to describe it? ? It is probably a thing you did a dozen times in Defold :smiley:

Copy a default sprite material, fragment and vertex program to your working folder. I donā€™t care about tidiness so much for this exercise. (But that alter ago speaking in brackets will be watching you! :eye: Remember for future that a good structure is crucial! :broom: ):

image

You can (should! :eye:) rename them. Open your new material and change the fragment program to your new fp (fragment program), however you named it (however?! :eye:):

Save it (save frequently! :eye:) and open your new fragment program. (and do backups! :eye:)

Magic explained line by line, just like here, but, you know, without Nerdā€™s babble (itā€™s a professional documentation! :eye:)
Itā€™s a code written in GLSL (OpenGL Shading Language), something similar to C++. You will notice I commented out some lines that are useless here. (if there is a useless code remove it! donā€™t leave comments! :eye:) For now just focus on the essence which is for each fragment to:

  1. Get a position from vertex program

  2. Get your image data

  3. Get a color of that particular pixel by sampling your texture at an interpolated position

  4. and write it to the output (gl_FragColor)

     /*1*/ varying mediump vec2 var_texcoord0; // position of your fragment = output from vertex shader = input for fragment shader
    
     /*2*/ uniform lowp sampler2D texture_sampler;  // your image = texture data used like a reference here to create an image = this is automatically assigned by Defold engine, because you specified an image for a sprite
     //uniform lowp vec4 tint;  //vector representing a color in RGBA (red, green, blue, alpha)
     // above are uniform variables - passed to the fragment program (first by engine, second by user)
    
     void main() //this is hmm, a main function of your program, it looks like it has no output (void), but you'll know it's not particularly true
     {
         //lowp vec4 tint_pm = vec4(tint.xyz * tint.w, tint.w);
    
         /*3+4*/ gl_FragColor = texture2D(texture_sampler, var_texcoord0.xy); // * tint_pm;  I commented out multiplying the color by tint, texture2D() is a function that takes sampler data and interpolated position and gives you a color of the fragment as a vector in RGBA (red, green blue, alpha)
     }
    

Save it. Open you collection with a game object with a sprite and change its Material property to your new material.

image

If everything is ok, you should see a sprite as it was. Nothing changed. Itā€™s because we got the texture data and put it to a graphic card just like we received it. So now, letā€™s have a fun:
Get back to your fragment program and add in the last line of main:

gl_FragColor = texture2D(texture_sampler, var_texcoord0.xy) * vec4(1.0, 0.0, 0.0, 1.0);

So I just added * vec4(1.0, 0.0, 0.0, 1.0) - multiplied the color by another color (red, because vector represents the color in RGBA with normalized values, so full red, no green, no blue and non-transparent) so whatever the color of the pixel was it will have 0 * blue color and 0 * green color. Save it. Then check in your collection whether the sprite has changed (ooh I love Defold :heart: , you donā€™t need to build your project to check, if such simple shader is working, changes are visible in Editor immediately!). Your image should be more aggressive now:

Thatā€™s it, you wrote your first post-process effect! But we wanted to blink in white, so letā€™s change the values in the vector to: (1.0, 1.0, 1.0, 1.0) - and you will see that this will not affect you sprite much - because any value multiplied by 1.0 will be still the same value. You can tinker a little bit with the vector, probably you will get to the solution that you could multiply it by e.g. 10.0 (value in the end will be normalized anyway) to get a very bright image, but itā€™s still not pure white everywhere.

So, just try to leave a vector part here:

gl_FragColor = vec4(1., 1., 1., 1.)

You will get:

Itā€™s because you set the colour of every pixel of that texture to white. And you see that the texture is rectangular, even if you have a character on a transparent background. Letā€™s assume for now you have some transparent part, ok? Hmm. Use that tip to distinguish background and sprite!

Check, if colour of the pixel has alpha = 0 and if so, use the old way for setting a colour, if not, itā€™s a part of your character, so set the colour to white. I will tidy up the code a little bit (finally! :eye:)

varying mediump vec2 var_texcoord0; // as it was
uniform lowp sampler2D texture_sampler; // as it was

void main()
{
    //write a color of the current fragment to a variable (lowp = low precision (it's enough))
    lowp vec4 color_of_pixel = texture2D(texture_sampler, var_texcoord0.xy);

    if(color_of_pixel.a != 0.0) // when alpha value of the color is not 0 (not transparent) (you can get components of this vector like .r, .b, .g and .a)
    {
        color_of_pixel = vec4(1.,1.,1.,1.);  // then it's your character - turn the color to white
    }
    gl_FragColor = color_of_pixel;  // write the color_of_pixel to the output gl_FragColor
}

(Save it! :eye:) Result:

We finally hit that Minotaur! Audience is clapping! Critical hit!

The only thing left is that we should switch blink on and off.


šŸ—Ŗ Talking to the shady guy

We will tell the shader to turn on the blink effect and after some time to - turn it off.

We will do it by setting a constant* of a fragment program. If you didnā€™t removed it, you should already have on in your material - tint. We will use it. You can rename it however you like it (for example blink_effect_trigger :eye:)


*in runtime! - it should be confusing now, but if you will treat each run of the fragment program as a separate operation you will understand that it is indeed a constant specified before the ā€œruntimeā€ of the fragment program

Open your fragment program and add that constant as a uniform variable at the top (I hope you really renamed it! :eye:):

uniform lowp vec4 blink_effect_trigger;

Wrap your current if(){} statement with another if or just add another condition to your if() (letā€™s choose the second solution :eye:):

    if((blink_effect_trigger.r == 1.0) && (color_of_pixel.a != 0.0))
    {
        color_of_pixel = vec4(1.,1.,1.,1.);
    }

This will mean that only, when the X component of the vector blink_effect_trigger is set to 1.0, our post-process blink will be working.

Yes, you can test it without building and running your code, just in Defold Editor! Save everything and your sprite should be now white, but change the X of vector blink_effect_trigger to 0:


Click somewhere outside (to confirm your change) and save. Check out your sprite - it should be like on a color TV! :tv:
So we can now manually turn on and off our post-process! :partying_face:

Now you will need to create a Lua script:
image

And attach it to your game object with the sprite:
image

(Save everything :eye:)
Open your script and leave only init function. Write here only:

function init(self)
	go.set("#sprite", "blink_effect_trigger", vmath.vector4(1)) -- set property of "#sprite" component called "blink_effect_trigger" to vector of four components all equal to 1) = aka turn on your post-process
end

You told your sprite to set its constant blink_effect_trigger to vector with 1, meaning the post-process will be turned on!
Now, finally, after so much time without building our game - save it and build it!
You should see:

We just told that shady guy to turn on the post-process in runtime, at the very beginning of the program (aka in init)!

*Champagne pops! * :champagne:
But hey! Whereā€™s turning off the effect??
*Champagne floods the floor * :sweat_drops:

Ok, ok, hereā€™s a fancy script:

function init(self)
	go.set("#sprite", "blink_effect_trigger", vmath.vector4(0)) -- at first turn off the post-process

	timer.delay(1, true, function() -- every second
		
		go.set("#sprite", "blink_effect_trigger", vmath.vector4(1)) --blink 
		
		timer.delay(0.1, false, function()  --and after 0.1 sec
			go.set("#sprite", "blink_effect_trigger", vmath.vector4(0))  -- turn of the post-process
		end)
		
	end)
end

Save it, build it, make a gif and post it to social media announcing that you are working on Dark Souls in 2D and finally managed to hit your first boss: :smiley:

Aaaa, and open another champagne! :champagne:


:fishing_pole_and_fish: Summary

Itā€™s not perfect (you should listen to me instead, it would be! :eye:) and as I said, you should utilize shaders for much better effects - but hey, in few minutes you learned basics of computer graphics, OpenGL and how to write fragment programs, set up everything in Defold, test it, use shader constants and communicate with your shaders from a Lua script! Iā€™m proud of you!

I was using such shader for enemies hit indication in a current demo of my Witchcrafter and Iā€™m using fragment program on a hero sprite to modify his equipment even! And I wrote everything about it in details (with code) here!

Changing this:
png
to this:

Shaders are very powerful, just experiment!

And thank you so much for reading this!

Paweł

53 Likes

And the project I was working on:

Blink_shader.zip (29.2 KB)

6 Likes

Excellent, bookmarked.

3 Likes

Amazing! Thank you for putting this together!

Can we (with your permission) convert your post into a blog post? (you will be attributed as the author ofc)

4 Likes

Yes, it would be awesome! :star_struck:
Should I remove all the puns? :smile:

1 Like

Thanks! Iā€™ll start working on this next week I think.

No no! I think itā€™s great!

3 Likes

Awesome article!

3 Likes

Thank you guys! :grin:

2 Likes

Thank you for this. Really great post.

4 Likes

You are awesome!

3 Likes

I like your style of your toturials.
Expect more.

1 Like

Really, nice tutorialā€¦
I begun to do thisā€¦
thankx a lotā€¦

1 Like

Thank you guys! I would love to make more, however itā€™s taking so much time, Iā€™m always wanting to spend it with my family or on my game, but I will try to write more now :blush: There is a video about Defold that I started like two months ago and still didnā€™t finished! :sweat_smile: I must organise some time for it :grin: And welcome to the forum @datamonster96 :grin:

3 Likes

Weā€™ve actually discussed some kind of collaborative blog system where the best posts each month get a small reward to incentivise content creation. @AGulev is a big fan of this and I believe it could work really well for the Defold community.

9 Likes

That could be another motivation! :+1:

1 Like

Good idea! But I think these tutorials could be posted both on Defoldā€™s forum and other few places (like youtube (defoldvideos acc?), or short videos on Twitter idk). Just to help Defold grows.

I do have plans to make a few short tutorials but Iā€™m noob rn (yea even after finish a few games and prototypes)
(Thanks for the awesome tutorial btw @Pawel )

1 Like

We want to spread knowledge of Defold to as many places as possible, but what we have learnt is that creating high quality video content takes a lot of time and effort, and our bandwidth is limited. And for video content we already have great video content creators in the community, like @sergey.lerg and @dchadwick , and also recently Iā€™ve seen video tutorials in Portuguese.

What we have toyed with is the idea of really short (max 5 minute) tutorials/videos showing hands on use of Defold, focusing on a single feature, concept, component or thing. I think this would make it more manageable and it can be created when we have some time to spare in between other tasks.

But one thing doesnā€™t exclude another right? I believe that a collaborative blog series with content from both the Defold Foundation and the community would be of interest to a lot of users.

3 Likes

Awesome tutorial! Iā€™ve always wanted to learn about shaders, so I took the time to follow along. Certainly have given me the itch to experiment further :clap:

If anyone is following along with a smoother image (non-pixel art), this code preserves the smooth edges.

varying mediump vec2 var_texcoord0;

uniform lowp sampler2D texture_sampler;
uniform lowp vec4 blink_effect_trigger;

void main()
{
    lowp vec4 color_of_pixel  = texture2D(texture_sampler, var_texcoord0.xy);

    if (blink_effect_trigger.r == 1) {
        color_of_pixel = vec4(1.0) * color_of_pixel.a; // White * Alpha
    }
    
    gl_FragColor = color_of_pixel;
}

Resulting image:
image

2 Likes

@britzl I think that community tutorials and vlogs like Brackeys, GFS, Thomas Brush or Black Thornpod have are awesome, something like this for Defold would mean a lot - you know, generic gamedev topics explained, but with addition of Defold examples :heart:

Both ideas of blogs and vlogs are awesome! I would for sure make some more tutorials!

@SkaterDad great! And thanks for sharing your solution! :heart:

3 Likes

It would be helpful if this tutorial or another addressed how to use var_texcoord0 because this has peculiarities to defold. In particular, it would be easier for new users if they were aware of:

  1. ā€˜var_texcoord0ā€™ is relative to an entire atlas. So if I try adding more than one sprite to the atlas in the project pawel uploaded, using shaders got more complicated. I have yet to see a solution for this.

  2. Difference between global and local position coordinate space. I have read that we donā€™t have access to local coordinate space in shadersā€“I am not sure if this info is outdated, but I seem to recall having trouble with this, but canā€™t recreate it right now. (Also: coordinates need to be scaled to the aspect ratio of the texture)

4 Likes