• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!

Shaders Normal mapping with tiles (Shaders)

M

MilkyBrain

Guest
First of all, I am not very good in shaders to write everything on my own so I tried to find any information about normal mapping shaders in game maker as I wanted them so badly to see in my project and decided to stop on a quite good example from NapalmIgnition in thread:
http://gmc.yoyogames.com/index.php?showtopic=586231&p=4724473 #17
Ok I ment to do this sooner. as a few people have asked recently I thought I would post it here.
Here is how I modified the above code to make it a little more dynamic

Light Controls

So first you need a parent object for all the lights, for me this was completely empty and called Light_Controller.

To create a Dynamic light:
In the create event you need to set the colour and the "height"
You need to set the parent to Light_Controller
RGB are the light intensities and lz is the lights height above the objects.

That's it. you can make the lights move or change the colours, intensities and height with each step but a basic light just needs those 4 variables

lz = 20;
R=6;
G=2;
B=0.0;

To create an object to be lit:

the texture of the object has to be set to 3D and a power of 2.

In the create step you need to define Light and Lights. Light is 1/0 for on/off and Lights is the number of dynamic lights up to 4. 0 Lights is just sunlight

Light=1
Lights=4

Run the next code in the end step event. ( I have it set to a script with no arguments for simplicity)

This code checks if lighting is turned on, runs through all of the lights and sorts them in to the 4 closest. If there is less than 4 or the max dynamic lights setting is lower this is recorded.

//Initialise/Zero the variables used
ID1=0;ID2=0;ID3=0;ID4=0;
DIST1=500;DIST2=500;DIST3=500;DIST4=500;max_range=500;
light_number = 0;



//Sort the lights and keep the 4 closest(could be modified for the strongest)
if(Lights>=1){
with(Light_Controller)
{
other.DIST=point_distance(x,y,other.x,other.y)
if (other.DIST<other.max_range)//This "if" jumps straight to the end
{
other.Done=0
other.light_number = other.light_number+1
if (other.DIST<other.DIST1)
{
other.DIST4=other.DIST3 ; other.ID4=other.ID3
other.DIST3=other.DIST2 ; other.ID3=other.ID2
other.DIST2=other.DIST1 ; other.ID2=other.ID1
other.DIST1=other.DIST ; other.ID1=self.id
other.Done = 1
}
if (other.Done=0&&other.Lights>=2){//This "if" jumps straight to the end
if (other.DIST<other.DIST2)
{
other.DIST4=other.DIST3 ; other.ID4=other.ID3
other.DIST3=other.DIST2 ; other.ID3=other.ID2
other.DIST2=other.DIST ; other.ID2=self.id
other.Done = 1
}
if (other.Done=0&&other.Lights>=3){//This "if" jumps straight to the end
if (other.DIST<other.DIST3)
{
other.DIST4=other.DIST3 ; other.ID4=other.ID3
other.DIST3=other.DIST ; other.ID3=self.id
other.Done = 1
}
if (other.Done=0&&other.Lights>=4){//This "if" jumps straight to the end
if (other.DIST<other.DIST4)
{
other.DIST4=other.DIST ; other.ID4=self.id
}
}}}}//All the "ifs" end here
}}
light_number=min(light_number,Lights)

Then in the draw event I have a 3 argument script. the first argument is the diffuse texture the second is the normal texture and the final is the angle or direction.

This script takes all of the variables defined previously, passes them on to the shader and runs the shader.

if(Light=1)
{
//draw_dynamic_light(Diffuse_Texture,Normal_Texture,Angle_of_Sprite)
//Get Variables from the Shader
//Get Variables for the object to be drawn
sampler = shader_get_sampler_index(s_light, "s_normalmap")
bg = sprite_get_texture(argument1,0);
angle = shader_get_uniform(s_light,"angle");
//Get Variables for the lights
light_num = shader_get_uniform(s_light,"light_num");
light_pos1 = shader_get_uniform(s_light,"light_pos1");
light_dif1 = shader_get_uniform(s_light,"light_dif1");
light_pos2 = shader_get_uniform(s_light,"light_pos2");
light_dif2 = shader_get_uniform(s_light,"light_dif2");
light_pos3 = shader_get_uniform(s_light,"light_pos3");
light_dif3 = shader_get_uniform(s_light,"light_dif3");
light_pos4 = shader_get_uniform(s_light,"light_pos4");
light_dif4 = shader_get_uniform(s_light,"light_dif4");



//Set which shader to use
shader_set(s_light);
//Set object variables
texture_set_stage(sampler,bg);
shader_set_uniform_f(angle,degtorad(argument2));
//Number of lights
shader_set_uniform_f(light_num,light_number);
//Light 1
if (light_number >= 1)
{
shader_set_uniform_f(light_pos1,ID1.x,ID1.y,ID1.lz);
shader_set_uniform_f(light_dif1,ID1.R,ID1.G,ID1.B);
}
//Light 2
if (light_number >= 2)
{
shader_set_uniform_f(light_pos2,ID2.x,ID2.y,ID2.lz);
shader_set_uniform_f(light_dif2,ID2.R,ID2.G,ID2.B);
}
//Light 3
if (light_number >= 3)
{
shader_set_uniform_f(light_pos3,ID3.x,ID3.y,ID3.lz);
shader_set_uniform_f(light_dif3,ID3.R,ID3.G,ID3.B);
}
//Light 4
if (light_number >= 4)
{
shader_set_uniform_f(light_pos4,ID4.x,ID4.y,ID4.lz);
shader_set_uniform_f(light_dif4,ID4.R,ID4.G,ID4.B);
}
//Draw the sprite
draw_sprite_ext(argument0,0,x,y,1,1,argument2,image_blend,image_alpha)
shader_reset();
}
else
{
draw_sprite_ext(argument0,0,x,y,1,1,argument2,image_blend,image_alpha)
}

In the Shader

The Vertex shader is a straight pass through. doesn't do anything but pass on values to the fragment.

attribute vec3 in_Position; // (x,y,z)
attribute vec4 in_Colour; // (r,g,b,a)
attribute vec2 in_TextureCoord; // (u,v)



varying vec2 v_texcoord;
varying vec4 v_color;
varying vec4 v_pos;

uniform sampler2D s_normalmap;

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;

//pass colour, texture coords and fragment position in the world
v_color = in_Colour;
v_texcoord = in_TextureCoord;
v_pos = gm_Matrices[MATRIX_WORLD] * object_space_pos;
}

In the fragment shader it looks complicated but most of it is just repeating the calcs for each light as I was strongly advised against using for/while statement in a shader. A few lighting constants are defined in the shader.
1.In the setup section, resolution scales the light attenuation. attenuation has 3 variables that make a quadratic equation for the light attenuation. ambient_color sets how dark everything Is without any lights. sun_colour sets the brightness and colour of the sun.
2.In the Sun section. set to_transform to an x and y position of the sun relative to the objects. delta_pos has the "height" value

That's it, all the dynamic lights sort them selves from the variables defined earlier.

varying vec2 v_texcoord;
varying vec4 v_color;
varying vec4 v_pos;



uniform sampler2D s_normalmap;
uniform float angle;

uniform float light_num;
uniform vec3 light_pos1;
uniform vec3 light_dif1;
uniform vec3 light_pos2;
uniform vec3 light_dif2;
uniform vec3 light_pos3;
uniform vec3 light_dif3;
uniform vec3 light_pos4;
uniform vec3 light_dif4;

void main()
{
//This is the Setup Section ----------------------------------------------------
//setup variables
vec2 pos = v_pos.xy;
vec2 resolution = vec2(512.0,512.0);
vec3 attenuation = vec3(0.5,5.0,20.0);
vec3 ambient_color = vec3(0.2,0.2,0.2);
vec3 sun_color = vec3(0.7,0.7,0.7);
//Per light variables
vec3 light = light_pos1;
vec3 light_color = light_dif1;

//sample the textures
vec4 t_color = texture2D(gm_BaseTexture,v_texcoord.xy);
vec3 n_color = texture2D(s_normalmap,v_texcoord.xy).xyz;
//Convert normals to correct range
vec3 normal = normalize(n_color * 2.0 - 1.0);

//THIS IS THE SUN SECTION-------------------------------------------------------

//Find the light vector
vec2 to_transform = vec2(100.0,100.0);
//Transform the light vector with the angle (cc and ss are cos/sin of the object and used repetedly)
float cc = cos(angle);
float ss = sin(angle);
to_transform = vec2(to_transform.x*cc-to_transform.y*ss,to_transform.x*ss+to_transform.y*cc);
//Normalize, take dot product
vec3 delta_pos = vec3(to_transform.xy,20.0);
vec3 light_dir = normalize(delta_pos);
float lambert = clamp(dot(normal,light_dir), 0.0, 1.0);
//Attenuation is unused in sun lighting but it initialises the variables
float d = sqrt(dot(delta_pos,delta_pos))/resolution.x;
float att = 1.0 / (attenuation.x+(attenuation.y*d)+(attenuation.z*d*d));
//Set the result
vec3 result = ambient_color + (sun_color.rgb * lambert);

//THIS IS THE LIGHT1 SECTION----------------------------------------------------

if (light_num >= 1.0)
{
//Easily visable light variables go here
light = light_pos1;
light_color = light_dif1;
//Find the light vector
to_transform = vec2(pos.xy-light.xy);
//Transform the light vector with the angle
to_transform = vec2(to_transform.x*cc-to_transform.y*ss,to_transform.x*ss+to_transform.y*cc);
//Normalize, take dot product
delta_pos = vec3(to_transform.xy, light.z);
light_dir = normalize(delta_pos);
lambert = clamp(dot(normal,light_dir), 0.0, 1.0);
//Attenuation from lights position
d = sqrt(dot(delta_pos,delta_pos))/resolution.x;
att = 1.0 / (attenuation.x+(attenuation.y*d)+(attenuation.z*d*d));
//Add the result to the sun light
result = result + (light_color.rgb * lambert * att);
}

//THIS IS THE LIGHT2 SECTION----------------------------------------------------

if (light_num >= 2.0)
{
//Easily visable light variables go here
light = light_pos2;
light_color = light_dif2;
//Find the light vector
to_transform = vec2(pos.xy-light.xy);
//Transform the light vector with the angle
to_transform = vec2(to_transform.x*cc-to_transform.y*ss,to_transform.x*ss+to_transform.y*cc);
//Normalize, take dot product
delta_pos = vec3(to_transform.xy, light.z);
light_dir = normalize(delta_pos);
lambert = clamp(dot(normal,light_dir), 0.0, 1.0);
//Attenuation from lights position
d = sqrt(dot(delta_pos,delta_pos))/resolution.x;
att = 1.0 / (attenuation.x+(attenuation.y*d)+(attenuation.z*d*d));
//Add the result to the sun light
result = result + (light_color.rgb * lambert * att);
}

//THIS IS THE LIGHT3 SECTION----------------------------------------------------

if (light_num >= 3.0)
{
//Easily visable light variables go here
light = light_pos3;
light_color = light_dif3;
//Find the light vector
to_transform = vec2(pos.xy-light.xy);
//Transform the light vector with the angle
to_transform = vec2(to_transform.x*cc-to_transform.y*ss,to_transform.x*ss+to_transform.y*cc);
//Normalize, take dot product
delta_pos = vec3(to_transform.xy, light.z);
light_dir = normalize(delta_pos);
lambert = clamp(dot(normal,light_dir), 0.0, 1.0);
//Attenuation from lights position
d = sqrt(dot(delta_pos,delta_pos))/resolution.x;
att = 1.0 / (attenuation.x+(attenuation.y*d)+(attenuation.z*d*d));
//Add the result to the sun light
result = result + (light_color.rgb * lambert * att);
}

//THIS IS THE LIGHT4 SECTION----------------------------------------------------

if (light_num >= 4.0)
{
//Easily visable light variables go here
light = light_pos4;
light_color = light_dif4;
//Find the light vector
to_transform = vec2(pos.xy-light.xy);
//Transform the light vector with the angle
to_transform = vec2(to_transform.x*cc-to_transform.y*ss,to_transform.x*ss+to_transform.y*cc);
//Normalize, take dot product
delta_pos = vec3(to_transform.xy, light.z);
light_dir = normalize(delta_pos);
lambert = clamp(dot(normal,light_dir), 0.0, 1.0);
//Attenuation from lights position
d = sqrt(dot(delta_pos,delta_pos))/resolution.x;
att = 1.0 / (attenuation.x+(attenuation.y*d)+(attenuation.z*d*d));
//Add the result to the sun light
result = result + (light_color.rgb * lambert * att);
}

//THIS IS THE FINAL ADDITION----------------------------------------------------

//Final color
result = result * t_color.rgb;
//Output
gl_FragColor = v_color * vec4(result,t_color.a);
}

Hopefully that should work. Most of it is reasonably well commented so people should be able to work out any changes you want to make.

if anyone can add any improvements let me know


This works on my galaxy SIII mini (android) at 480*800 and a reasonable frame rate as long as your careful with the number of lights and objects

P.S. to make the normal map from image you can use: https://cpetry.github.io/NormalMap-Online/



After that I tryied that code in my project and it worked perfectly. However, it worked with objects but I wanted them to work with tiles.

So i tried to find more information about using shaders with tiles and found a thread on reddit form PixelatedPope:
https://www.reddit.com/r/gamemaker/comments/317ghv/want_to_apply_shaders_to_tile_layers_heres_how/
So, people who use my palette swap shader often ask me how they can apply it to their tiles... and up until this morning I told them that they really couldn't unless they were willing to redraw the application surface with the shader.

But this morning, I figured out a work around! It's not built into the palette swap shader package yet, but the solution would work for ANY shader.

Say you have two "groups" of tile layers. Your tiles that appear below your game objects and tiles that appear above your game objects.

Let's pretend you have 3 of each: Depth 5000, 4000, and 3000 for the lower group. And -3000, -4000, and -5000 for the "above" group.

You would need to create objects that "sandwich" those layer groups. For example, to enable a shader on the lower group, you would create an object with a depth of 5001 and 2999.

In the 5001 object, in the draw event, enable the shader.

In the 2999 object, in the draw event, disable the shader.


Bam. You just applied a shader to 3 tile layers without affecting everything else!

You would do the same thing with the upper group. An object at depth -2999 to turn it on, and an object at -5001 to turn it back off. Works perfectly!

Go forth and shade!

DONE....emm...ok

Ok sandwich objects for tile layers done and in the first object "copy-past"ed all code from object that is lit but made few changes in draw event script.

Just placed background normal texture instead of sprite normal texture and commented(you can just delete them) sprite drawings with shader_reset() , also removed direction argument as tiles are static.
WARNING! The texture of the tiles has to be set to 3D and a power of 2.

changes in draw event script:
///scr_draw_light(norm)

if(Light=1)
{
//draw_dynamic_light(Diffuse_Texture,Normal_Texture,Angle_of_Sprite)
//Get Variables from the Shader
//Get Variables for the object to be drawn
sampler = shader_get_sampler_index(shd_light, "s_normalmap")
bg = background_get_texture(argument0);
angle = shader_get_uniform(shd_light,"angle");
//Get Variables for the lights
light_num = shader_get_uniform(shd_light,"light_num");
light_pos1 = shader_get_uniform(shd_light,"light_pos1");
light_dif1 = shader_get_uniform(shd_light,"light_dif1");
light_pos2 = shader_get_uniform(shd_light,"light_pos2");
light_dif2 = shader_get_uniform(shd_light,"light_dif2");
light_pos3 = shader_get_uniform(shd_light,"light_pos3");
light_dif3 = shader_get_uniform(shd_light,"light_dif3");
light_pos4 = shader_get_uniform(shd_light,"light_pos4");
light_dif4 = shader_get_uniform(shd_light,"light_dif4");
//Set which shader to use
shader_set(shd_light);
//Set object variables
texture_set_stage(sampler,bg);
shader_set_uniform_f(angle,degtorad(0));
//Number of lights
shader_set_uniform_f(light_num,light_number);
//Light 1
if (light_number >= 1)
{
shader_set_uniform_f(light_pos1,ID1.x,ID1.y,ID1.lz);
shader_set_uniform_f(light_dif1,ID1.R,ID1.G,ID1.B);
}
//Light 2
if (light_number >= 2)
{
shader_set_uniform_f(light_pos2,ID2.x,ID2.y,ID2.lz);
shader_set_uniform_f(light_dif2,ID2.R,ID2.G,ID2.B);
}
//Light 3
if (light_number >= 3)
{
shader_set_uniform_f(light_pos3,ID3.x,ID3.y,ID3.lz);
shader_set_uniform_f(light_dif3,ID3.R,ID3.G,ID3.B);
}
//Light 4
if (light_number >= 4)
{
shader_set_uniform_f(light_pos4,ID4.x,ID4.y,ID4.lz);
shader_set_uniform_f(light_dif4,ID4.R,ID4.G,ID4.B);
}
//Draw the sprite
//draw_sprite_ext(argument0,0,x,y,1,1,0,image_blend,image_alpha)
//shader_reset();

}
else
{
//draw_sprite_ext(argument0,0,x,y,1,1,0,image_blend,image_alpha)
}
also in "end" sandwich object in draw event wrote shader_reset();
Next problem - light didn't appear on the tiles.
So I had almost the same problem as Bastendorf had:
https://forum.yoyogames.com/index.php?threads/solved-ish-normal-maps-with-tiles.13272/#post-89058 #10
if you were using LUX, I would say, set the tile layer to 1000000. add an object at depth 1000000 plus 1 (1 unit deeper) to turn on the shader (in the draw) with the tile background and the tile normal texture data.. and add another object at depth 1000000 minus 1 (1 unit above) to shader_reset() the shader...
Ah, PixelatedPope's shaders-on-tiles solution. I learned about that just this morning. I didn't think that would work, though. It seems to my I've-been-thinking-too-hard-and-it's-1-am brain that when the background and normal tiles are drawn, it would just draw the diffuse and normals images as solid overlay. Does that actually work? Would GameMaker spread the tiles out over their correct positions automatically, or would I have to tell it where to put them? Because I'm not manually telling GameMaker were to put potentially 16,384 tiles via code. That's some 9th Circle of Hell level stuff, right there. Wait, what's a LUX?


What the thing basically is that you want;

- Still use tiles (draw your diffuse on there)
- Draw the same tiles, but the normal map as well
- Merge them with a shader, and output that to the screen

Could you try? See where you're getting stuck? Or don't you know where to start? I'd love to help but its best if you do the thinking :) Thats how you learn
Actually, that's not how I learn. I learn by being hands on. I taught myself how to program at the level I can program at today, by taking apart other people's codes and figuring out how they work. (Mind you, I don't need to do that as much any more, but that's how I learned.)
Like I said above, though, to my tired brain, it looks like I'd have to tell GameMaker where to put the tiles in your example, too. I'm not a fan of having to code in the coordinates and IDs of 16,384 tiles. I could be completely mistaken on how your idea, works. I need rest before I can analyze for your ideas. So I guess I'll be back in the morning to take a second look at them.


I don't know why but I had some feelings that this is issue with tile drawing order or something like that.
So i just placed some tiles near the initial point coor(0,0) in the room and BANG!!! IT WORKS WITH TILES!!........wait, it works only till unknown zone.

I tried to investigate why light does not appear on tiles in the middle of the room for example and I had no clue. Until I moved accidently in the room editor "start shader object" to the right and light started to appear further...ok.

So I found next:


And decided to attach that "start sandwich object" to the center of the view.........after that everything worked well and everywhere. Only invisible object is flying in the middle of the view.

Here we are!


Conclusion - as I said before, I am not very good in shdaers so this is thread about how I reached what I needed and you can do it by your way.

At least, it works.

About project:
Twitter - https://twitter.com/Milky_Brain
Instagram - https://www.instagram.com/monolith_game/
DONE! :]
 

Attachments

Last edited by a moderator:
Top