Graphics Ultra-Fast 2D Dynamic Lighting System

GM Version: ALL (coded in 2.3. Some codes like the new functions needs to be tweaked for it to work for older versions)
Target Platform: Windows (Other platforms not tested. until HTML5 Z-buffer is fixed, this wont work in HTML5)
Download: Itch page
Links: Youtube

Summary:
2D Dynamic Lighting System, also called Realtime Lighting System or Shadow Casting, is a method of dynamically lighting and casting shadows. The same algorithm could be used for Line of Sight (like in Among Us). I will be showing my own implementation that could handle a thousand lights (5000 in my case) with little to no lag!

Video Intro:

Video Tutorial:

Required knowledge:
  • Shaders
  • Vertex Buffers
  • Blend modes
  • Z-Buffer Depth handling
Tutorial:
This implementation is similar to Mike Daily's Realtime 2D Lighting but with a couple of optimizations. I utilized the use of shaders to transfer the bulk of shadow construction from the CPU to the GPU. I then used the Z-buffer depth handling to draw all the lights and shadows to one single surface, solving the texture swap lag issues. I recommend watching the videos to get a better understanding of this tutorial.

We will need just three objects to make this work:
  • Setup object
  • Walls
  • Lights
Both Walls and Lights will have ZERO code. All the code will be written in the setup object.

Construct the room by simply dropping walls and stretching them to your liking (you can later add wall tiles over the object for aesthetics). Put as many lights as you want in the room. Lastly ,place one setup object into the room, preferably in an instance layer below the rest.
1604819313593.png

We will be drawing the lights with the use of shaders. This is how the light shader will look like:

Vertex Shader:
GML:
attribute vec3 in_Position;                  // (x,y,z)
varying vec2 pos;
uniform float u_z; //depth position

void main(){
    vec4 object_space_pos = vec4( in_Position.x, in_Position.y, u_z, 1.0);
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
    pos = in_Position.xy;

}
Fragment Shader:
GML:
varying vec2 pos; //current pixel position
uniform vec2 u_pos; //light source positon

const float zz = 32.; //larger zz, larger light

void main(){
    vec2 dis = pos - u_pos;
    float str = 1./(sqrt(dis.x*dis.x + dis.y*dis.y + zz*zz)-zz); //strength of light is the inverse distance
    gl_FragColor = vec4(vec3(str),1.);
}
This will draw a bright white light at the center of the light and it gets dimmer the further away we get from the center. The light strength is inverse to the distance from the light source. Uniform vec2 u_pos is the position of the light source. the constant float zz determines how large the light is (visual explanation in the tutorial video at 9:08). You can change this constant to be a uniform float so different lights can have different light sizes. We will talk about uniform float u_z later.

To draw our shadows, we will use vertex buffers. We setup the vertex buffer in the create event of the setup object setup like so:
GML:
//Vertex format and buffer setup
vertex_format_begin();
vertex_format_add_position_3d();
vf = vertex_format_end();
vb = vertex_create_buffer();
The vertex color and texcoord is not necessary for shadow construction so our vertex format will not include both. Note that we are using position 3D and not 2D. We will be using the Z coordinate as a flag to determine if we need to later reposition the vertex in the vertex shader (This is where the magic begins).

Then we create a function that can draw our quads as so:
GML:
//Creates Quad with two triangles. Used to make the shadows.
//Z coordinate is used as a flag to determine if the vertex will be repositioned in the shader
function Quad(_vb,_x1,_y1,_x2,_y2){
    //Upper triangle
    vertex_position_3d(_vb,_x1,_y1,0);
    vertex_position_3d(_vb,_x1,_y1,1); //repositioned vertex
    vertex_position_3d(_vb,_x2,_y2,0);

    //Lower Triangle
    vertex_position_3d(_vb,_x1,_y1,1); //repositioned vertex
    vertex_position_3d(_vb,_x2,_y2,0);
    vertex_position_3d(_vb,_x2,_y2,1); //repositioned vertex
}
_vb is the vertex buffer variable. x1 y1 represents one end of the wall and x2 y2 represents the other end of the wall. If you inspect the code carefully, you will notice that the vertices will either have both x1 and y1 or both x2 and y2, never a mixture of x1 and y2 or x2 and y1. If you forget the z coordinate, this will create a single line. Don't worry, this is intentional. Those with z position at 1 will be later repositioned in the shader so the quad will be drawn. This quad forms our shadows.

With each wall, we will draw the quads using the diagonals of the block. No, not the four edges, the diagonals. It's a simple optimization that works and reduces the amount of quads constructed by half. This is done in the object setup step function. We will do it like so:
GML:
//Construct the vertex buffer with every wall
//Instead of using the four edges as the walls, we use the diagonals instead (Optimization)
vertex_begin(vb,vf);
var _vb = vb;
with(obj_wall){
    Quad(_vb,x,y,x+sprite_width,y+sprite_height); //Negative Slope Diagonal Wall
    Quad(_vb,x+sprite_width,y,x,y+sprite_height); //Positive Slope Diagonal Wall
}
vertex_end(vb);
Let us now create the shadow shader like so:
Vertex Shader:
GML:
attribute vec3 in_Position;                  // (x,y,z)

uniform vec2 u_pos; //light source positon
uniform float u_z; //depth position

void main(){
    vec2 pos = in_Position.xy;

    if (in_Position.z > 0.){ //check if vertex requires repositioning
        vec2 dis = pos - u_pos;
        pos += dis/sqrt(dis.x*dis.x + dis.y*dis.y) * 100000.; //repositioning the vertex with respect to the light position
    }
    vec4 object_space_pos = vec4( pos.x, pos.y, u_z-0.5, 1.0); //shadow is drawn at a z-value closer to the screen than its corresponding light.
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
}
Fragment Shader:
GML:
void main(){
    gl_FragColor = vec4(0.); //draws an invisible shadow that can block the light when Z-buffer is on
}
What this shader does is, if the vertex that was passed through the shader contains a z coordinate that is greater than zero, it will reposition the vertex x and y position based on the light position, u_pos. This is very similar to how Mike Daily positioned his vertices. We get the vector distance from the light source to the end of the wall then divide it by the the scalar distance to get the unit vector. Multiply it with a large number and you get a vector with the same slope as the distance but with a very long length. We add this length to the wall end points and we get the end points of the shadow.

The big difference with my method compared to Mike Daily's is mine is calculated in the GPU. I will need to only create one common shadow vertex buffer for all lights instead of creating unique vertex buffers for every light. this common vertex buffer will be reconfigured differently through the shaders by changing the light position passed through the shader.

Note that we are drawing an invisible shadow (fragment shader code) but don't worry, this shadow will be used to block its corresponding light without affecting the previous lights, that's how we can draw all the lights directly without needing to draw on a separate surface. But in order to do this, we will need to utilize the Z-buffer to handle the depth.

Before we go to the draw function, let us first setup the shader uniform variables in the setup obejct create:
GML:
//Shader uniform variable setup
u_pos = shader_get_uniform(shd_light,"u_pos");
u_pos2 = shader_get_uniform(shd_shadow,"u_pos");
u_z = shader_get_uniform(shd_light,"u_z");
u_z2 = shader_get_uniform(shd_shadow,"u_z");
Here is the draw function of the setup object which draws the shadows first, then the light.
GML:
//Local variables setup
var _u_pos = u_pos;
var _u_pos2 = u_pos2;
var _u_z = u_z;
var _u_z2 = u_z2;
var _vb = vb;

//Turn on the Zbuffer (3D)
gpu_set_ztestenable(1);
gpu_set_zwriteenable(1);
var _z = 0;
with(obj_light){

    //Draw the shadows (AKA light blockers)
    shader_set(shd_shadow);
    shader_set_uniform_f(_u_pos2,x,y);
    shader_set_uniform_f(_u_z2,_z);
    vertex_submit(_vb,pr_trianglelist,-1);

    //Draw the Light
    gpu_set_blendmode(bm_add);
    shader_set(shd_light);
    shader_set_uniform_f(_u_pos,x,y);
    shader_set_uniform_f(_u_z,_z);
    draw_rectangle(0,0,320,180,0); //canvas for drawing the light
    gpu_set_blendmode(bm_normal);

    _z--; //Next set of shadows and lights is set closer to the screen
}
shader_reset();
gpu_set_ztestenable(0);
gpu_set_zwriteenable(0);
we turn on ztestenable and zwriteenable to allow the use of Z-buffer depth handling. Remember the u_z variable in the light shader? it is also present in the shadow shader. the only difference is that the shadow sets its z coordinate to u_z -0.5, while the light sets its shadow to just u_z. This lets the shadow be drawn over the light, even if the shadow is drawn first. The cool thing is, even if the shadow is transparent, the light will not be able to draw over the shadow. The lights previously drawn that are under the shadow will not be affected since the shadow is transparent. after drawing each light and shadow pair, we bring the _z value closer to the screen so the next light will be over the previous shadows.

Lastly, remember that the light should be drawn with additive blend since that's how lights work. We don't want the lights to replace other lights but rather merge with other lights. The rectangle is set to be the view width and height. Right now, it is simply set at (0,0,320,180) but this should be changed depending on the view dimensions and position.

And that's it! Although we used several advanced concepts like shaders and vertex buffers, the code is relatively simple, yet effective. We can further improve this implementation with post-processing. I was able to implement wall edge lighting which I plan on sharing some other time :) Understand that this system works best with 90 degree edges. It is possible to do this with other polygons or rotated blocks but there will need to be a few tweaks to the code. This system unfortunately cannot handle irregular shaped walls.
 
Last edited:

Zhanghua

Member
Wonderful Tutorial

@
GrizzliusMaximus
In the shd_light.fsh, why the "varying vec2 pos;" can get the current pixel position?
Isn't it the Coner Position of the View Rectangle which size is 320*180?

I kown that..
The varying variable both existed in the VSH and FSH will be interpolated during the raster, then the new value will pip to the FSH.
While the VSH is only excuted for the submitted vetrics.
 
I kown that..
The varying variable both existed in the VSH and FSH will be interpolated during the raster, then the new value will pip to the FSH.
While the VSH is only excuted for the submitted vetrics.
Sorry for the late reply. Didn't you answer your own question? in the vertex shader, the varying vector is set as the corner pixel, which is interpolated in the fragment shader. the varying vector in the fragment shader should be pointing to the pixel position in game.
 

Zhanghua

Member
Sorry for the late reply. Didn't you answer your own question? in the vertex shader, the varying vector is set as the corner pixel, which is interpolated in the fragment shader. the varying vector in the fragment shader should be pointing to the pixel position in game.
Thank you for your detail. I had know the vertex shader manipulate the corners, but unknown the interpolated part between the VS and FS before.

TKS again.
 
Excellent tutorial. I was in the middle of a deep dive into 2D lighting systems, very slowly piecing together enough knowledge for a basic lighting setup. Then I see this popup in one of my google searches...Lo and behold, not only is it way better than what I would've managed to make on my own with my limited skill set, but it's specifically for GMS so I don't have even have to do any language conversion!

I know how much effort it must've taken to get to the point where you can make this for yourself, so I could totally understand wanting to keep that knowledge in a deep dark place that you can bust out to make your game shine over others...However, you've unleashed your learnings on the rest of us for free and easily digestible. Hats off to you my self-less friend. If you don't have any objections, once I've gotten the lighting established in my game I'd like to make a blog post on my site about it to try to drive some of my meager traffic over to your youtube channel (I won't share any code, simply some example images and a write-up about how you're responsible for making the system with linkage). Lemme know if you're cool with that or not =)
 
Excellent tutorial. I was in the middle of a deep dive into 2D lighting systems, very slowly piecing together enough knowledge for a basic lighting setup. Then I see this popup in one of my google searches...Lo and behold, not only is it way better than what I would've managed to make on my own with my limited skill set, but it's specifically for GMS so I don't have even have to do any language conversion!

I know how much effort it must've taken to get to the point where you can make this for yourself, so I could totally understand wanting to keep that knowledge in a deep dark place that you can bust out to make your game shine over others...However, you've unleashed your learnings on the rest of us for free and easily digestible. Hats off to you my self-less friend. If you don't have any objections, once I've gotten the lighting established in my game I'd like to make a blog post on my site about it to try to drive some of my meager traffic over to your youtube channel (I won't share any code, simply some example images and a write-up about how you're responsible for making the system with linkage). Lemme know if you're cool with that or not =)
Thanks! This comment really made my day. I also have no objections on you making a blog post about it! The point of my video tutorials is help as much people as I can. Yes, I must confess, I was very much tempted to keep all of this knowledge to myself. I had fears that somebody more successful than I will be able to make a game using my system and make more money out of it. And it really is a big possibility as I haven't made any money from my games asides from the meager donations. But then I thought to myself, how selfish can I be? We only live once, so why not live for others instead of myself? If I kept this knowledge to myself, only my games will benefit from it. but if I shared it, then a ton of games will benefit from my knowledge. What good is knowledge if it will die with me?

I would also like to say that the lighting system has evolved over time through the suggestions of others who have watched my video. In a sense, they too have contributed to the lighting system. My favorite suggestion are those found on part 5 of my tutorial. It was a real big game changer, I loved it so much, I added them to my own game, Beartopia.

Moral lesson is, help others and others will help you in ways you never knew. It is something that money cannot buy. To those reading this, I hope I inspired you to help others in what ever shape or form. You won't regret it, I promise you that.
 
Is there a way to implement this into the legacy version? sepalpha doesn't exist on the legacy version
Sorry for the late reply. There are ways to do it, I made an earlier version of this lighting system on GMS 1.4 but GMS 2 makes it a lot easier. Try using draw_set_colour_write_enable() together with blendmodes to get the same effect. I can't really give a straight forward answer as it gets complicated. Like I said, it's possible to do it in GMS 1.4 but it's more tedious.
 
@GrizzliusMaximus, Hi there. In the introduction video on your Youtube channel you mentioned and showed the system working with edge lighting but you never actually covered it. Is this something you still plan to do? As far as I know none of the other lighting engines offer it.
 
Hi there. In the introduction video on your Youtube channel you mentioned and showed the system working with edge lighting but you never actually covered it. Is this something you still plan to do? As far as I know none of the other lighting engines offer it.
Sorry about that. I was able to implement such as seen in the intro, but after some testing, I found some flaws to it. I tried fixing it but it got more convoluted. I'll try to get back to it once I find a fix for it and get my priorities done (I just graduated from university and started working)
 
Sorry about that. I was able to implement such as seen in the intro, but after some testing, I found some flaws to it. I tried fixing it but it got more convoluted. I'll try to get back to it once I find a fix for it and get my priorities done (I just graduated from university and started working)
Not a problem at all. Thanks for the response.
And many congratulations on your graduation and new job! šŸ¾
 

Shoba

Member
Awesome tutorial series! I used it to add some lightning to my own game, though I skipped some features when adding to my game since both the soft shadows and normal maps did not really fit the pixel art style I was aiming for. I've also managed to add some edge lightning to my game thanks to the knowledge about shaders I've gained while watching this.
 

Shoba

Member
Would you be willing to share how you achieved that?
Sure, but I changed quite a bit how the whole lightning engine works. I've used Shaun Spalding approach to draw the shadow into a surface, then draw it on a certain layer. Basically you draw the shadow + light in a Surface. Shadow is black, light is color and white. Then you draw the part of the tileset you'd like to edge lighted in black (or a dark grey) over the shadow surface and use a bloom shader on the whole surface. That way the light will bleed onto the edges, but only the part which has light above it. The you draw the surface over your background and your foreground tile set with gpu_set_blendmode_ext(bm_zero,bm_src_color). Again, I'm not sure if this works with the bright lights or normal maps. Okay, actually I am 100% sure it does not work with the bright light due the way the blending works at the end, you can only make things darker with this solution. Also I think performance also goes down, since there are a lot of surfaces involved. Still, a ton faster then my last attempt which used exaclty 0 shaders and 100% surface shenanigans.
 

Shoba

Member
Would you be willing to share the code?
Oh god. No. My source is horrible to read, I wouldn't want anyone to go through that. If you really want it though, drop me a message, I'll see what I can do. Basically everything in my system is a mix from this lightning system, Shaun Spaldings [version] and a bloom shader I found [here].
 

Thmask

Member
Oh god. No. My source is horrible to read, I wouldn't want anyone to go through that. If you really want it though, drop me a message, I'll see what I can do. Basically everything in my system is a mix from this lightning system, Shaun Spaldings [version] and a bloom shader I found [here].
Yes, please I really want the code, no matter how bad it is. I want to try to finish his ( GrizzliusMaximus ) system, so then share, making it open-source to everyone, it would be an amazing lighting system if finished, so your code even being horrible to read would help me to understand deeper the whole thing.
 

Shoba

Member
Yes, please I really want the code, no matter how bad it is. I want to try to finish his ( GrizzliusMaximus ) system, so then share, making it open-source to everyone, it would be an amazing lighting system if finished, so your code even being horrible to read would help me to understand deeper the whole thing.
Not sure if my source will be any help for this though. I never added features like highlights and normal maps to my system, since I was going for a limited palette retro look. If I don't forget I'll send you a message with my current code once I get home.
 
hello I am new to programming and would like to ask if it is possible to change the color of the shadow
(if it makes sense, of course)
 

Yaazarai

Member
Brooo really called me out there. This is dope man! I'm working on a new implementation that has edge lighting as well, so I'll hit you up when I've got it finished! To be honest my implementation was more of a proof of concept, which is why I never added edge lighting, it's not really optimized very well for a professional game. Uses one-two surfaces per light which is nasty especially on mobile/console hardware.

This is much better for commercial games.
 
Thanks! This comment really made my day. I also have no objections on you making a blog post about it! The point of my video tutorials is help as much people as I can. Yes, I must confess, I was very much tempted to keep all of this knowledge to myself. I had fears that somebody more successful than I will be able to make a game using my system and make more money out of it. And it really is a big possibility as I haven't made any money from my games asides from the meager donations. But then I thought to myself, how selfish can I be? We only live once, so why not live for others instead of myself? If I kept this knowledge to myself, only my games will benefit from it. but if I shared it, then a ton of games will benefit from my knowledge. What good is knowledge if it will die with me?

I would also like to say that the lighting system has evolved over time through the suggestions of others who have watched my video. In a sense, they too have contributed to the lighting system. My favorite suggestion are those found on part 5 of my tutorial. It was a real big game changer, I loved it so much, I added them to my own game, Beartopia.

Moral lesson is, help others and others will help you in ways you never knew. It is something that money cannot buy. To those reading this, I hope I inspired you to help others in what ever shape or form. You won't regret it, I promise you that.

Hi GrizzliusMaximus,

This is the exact reason why this community is so great! It feels like a family and I love that you shared this lighting system with the world. I'm glad you liked the ideas about the vibrant lighting and normal mapping! I feel a bit flattered that you mention this. šŸ˜Š

I would love to see this tutorial series be finished. I hope you find some time to share more of your knowledge with the world and we're looking forward to part 6 and beyond! Thanks for all the effort you've put into it already! I really love this lighting system, and I bet this is the most powerful lighting system for GM yet.

Thanks and hopefully we'll see you soon in a new video!
 

Andrachie

Member
Does anyone know how I can put a max on the amount the vibrant lights stack? I really like how the vibrant lights look, but when you stack too many close together, it gets way too bright. I was wondering if it's possible to have a limit to brightness
 

Andrachie

Member
Does anyone know how I can put a max on the amount the vibrant lights stack? I really like how the vibrant lights look, but when you stack too many close together, it gets way too bright. I was wondering if it's possible to have a limit to brightness
Not sure if anyone will see this, but I think I came up with a decent solution. Use this equation to replace the strength equation in the light shader:
GML:
float str = (1./(pow(sqrt(dis.x*dis.x + dis.y*dis.y + zz*zz)-zz, 0.4)))-0.1;
For my game at least, I think it looks a lot more natural and doesn't create overly dramatic lights. It also gets rid of a variable, so you only need to define size, which I think is easier to deal with. It could be tweaked further, but it's a good start.
 

badwrong

Member
Hi GrizzliusMaximus,

This is the exact reason why this community is so great! It feels like a family and I love that you shared this lighting system with the world. I'm glad you liked the ideas about the vibrant lighting and normal mapping! I feel a bit flattered that you mention this. šŸ˜Š

I would love to see this tutorial series be finished. I hope you find some time to share more of your knowledge with the world and we're looking forward to part 6 and beyond! Thanks for all the effort you've put into it already! I really love this lighting system, and I bet this is the most powerful lighting system for GM yet.

Thanks and hopefully we'll see you soon in a new video!

Well, the sample rate goes up by one for every light... so it's probably the least powerful if you think about it.
 

Radonred

Member
GM Version: ALL (coded in 2.3. Some codes like the new functions needs to be tweaked for it to work for older versions)
Target Platform: Windows (Other platforms not tested. until HTML5 Z-buffer is fixed, this wont work in HTML5)
Download: Itch page
Links: Youtube

Summary:
2D Dynamic Lighting System, also called Realtime Lighting System or Shadow Casting, is a method of dynamically lighting and casting shadows. The same algorithm could be used for Line of Sight (like in Among Us). I will be showing my own implementation that could handle a thousand lights (5000 in my case) with little to no lag!

Video Intro:

Video Tutorial:

Required knowledge:
  • Shaders
  • Vertex Buffers
  • Blend modes
  • Z-Buffer Depth handling
Tutorial:
This implementation is similar to Mike Daily's Realtime 2D Lighting but with a couple of optimizations. I utilized the use of shaders to transfer the bulk of shadow construction from the CPU to the GPU. I then used the Z-buffer depth handling to draw all the lights and shadows to one single surface, solving the texture swap lag issues. I recommend watching the videos to get a better understanding of this tutorial.

We will need just three objects to make this work:
  • Setup object
  • Walls
  • Lights
Both Walls and Lights will have ZERO code. All the code will be written in the setup object.

Construct the room by simply dropping walls and stretching them to your liking (you can later add wall tiles over the object for aesthetics). Put as many lights as you want in the room. Lastly ,place one setup object into the room, preferably in an instance layer below the rest.

We will be drawing the lights with the use of shaders. This is how the light shader will look like:

Vertex Shader:
GML:
attribute vec3 in_Position;                  // (x,y,z)
varying vec2 pos;
uniform float u_z; //depth position

void main(){
    vec4 object_space_pos = vec4( in_Position.x, in_Position.y, u_z, 1.0);
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
    pos = in_Position.xy;

}
Fragment Shader:
GML:
varying vec2 pos; //current pixel position
uniform vec2 u_pos; //light source positon

const float zz = 32.; //larger zz, larger light

void main(){
    vec2 dis = pos - u_pos;
    float str = 1./(sqrt(dis.x*dis.x + dis.y*dis.y + zz*zz)-zz); //strength of light is the inverse distance
    gl_FragColor = vec4(vec3(str),1.);
}
This will draw a bright white light at the center of the light and it gets dimmer the further away we get from the center. The light strength is inverse to the distance from the light source. Uniform vec2 u_pos is the position of the light source. the constant float zz determines how large the light is (visual explanation in the tutorial video at 9:08). You can change this constant to be a uniform float so different lights can have different light sizes. We will talk about uniform float u_z later.

To draw our shadows, we will use vertex buffers. We setup the vertex buffer in the create event of the setup object setup like so:
GML:
//Vertex format and buffer setup
vertex_format_begin();
vertex_format_add_position_3d();
vf = vertex_format_end();
vb = vertex_create_buffer();
The vertex color and texcoord is not necessary for shadow construction so our vertex format will not include both. Note that we are using position 3D and not 2D. We will be using the Z coordinate as a flag to determine if we need to later reposition the vertex in the vertex shader (This is where the magic begins).

Then we create a function that can draw our quads as so:
GML:
//Creates Quad with two triangles. Used to make the shadows.
//Z coordinate is used as a flag to determine if the vertex will be repositioned in the shader
function Quad(_vb,_x1,_y1,_x2,_y2){
    //Upper triangle
    vertex_position_3d(_vb,_x1,_y1,0);
    vertex_position_3d(_vb,_x1,_y1,1); //repositioned vertex
    vertex_position_3d(_vb,_x2,_y2,0);

    //Lower Triangle
    vertex_position_3d(_vb,_x1,_y1,1); //repositioned vertex
    vertex_position_3d(_vb,_x2,_y2,0);
    vertex_position_3d(_vb,_x2,_y2,1); //repositioned vertex
}
_vb is the vertex buffer variable. x1 y1 represents one end of the wall and x2 y2 represents the other end of the wall. If you inspect the code carefully, you will notice that the vertices will either have both x1 and y1 or both x2 and y2, never a mixture of x1 and y2 or x2 and y1. If you forget the z coordinate, this will create a single line. Don't worry, this is intentional. Those with z position at 1 will be later repositioned in the shader so the quad will be drawn. This quad forms our shadows.

With each wall, we will draw the quads using the diagonals of the block. No, not the four edges, the diagonals. It's a simple optimization that works and reduces the amount of quads constructed by half. This is done in the object setup step function. We will do it like so:
GML:
//Construct the vertex buffer with every wall
//Instead of using the four edges as the walls, we use the diagonals instead (Optimization)
vertex_begin(vb,vf);
var _vb = vb;
with(obj_wall){
    Quad(_vb,x,y,x+sprite_width,y+sprite_height); //Negative Slope Diagonal Wall
    Quad(_vb,x+sprite_width,y,x,y+sprite_height); //Positive Slope Diagonal Wall
}
vertex_end(vb);
Let us now create the shadow shader like so:
Vertex Shader:
GML:
attribute vec3 in_Position;                  // (x,y,z)

uniform vec2 u_pos; //light source positon
uniform float u_z; //depth position

void main(){
    vec2 pos = in_Position.xy;

    if (in_Position.z > 0.){ //check if vertex requires repositioning
        vec2 dis = pos - u_pos;
        pos += dis/sqrt(dis.x*dis.x + dis.y*dis.y) * 100000.; //repositioning the vertex with respect to the light position
    }
    vec4 object_space_pos = vec4( pos.x, pos.y, u_z-0.5, 1.0); //shadow is drawn at a z-value closer to the screen than its corresponding light.
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
}
Fragment Shader:
GML:
void main(){
    gl_FragColor = vec4(0.); //draws an invisible shadow that can block the light when Z-buffer is on
}
What this shader does is, if the vertex that was passed through the shader contains a z coordinate that is greater than zero, it will reposition the vertex x and y position based on the light position, u_pos. This is very similar to how Mike Daily positioned his vertices. We get the vector distance from the light source to the end of the wall then divide it by the the scalar distance to get the unit vector. Multiply it with a large number and you get a vector with the same slope as the distance but with a very long length. We add this length to the wall end points and we get the end points of the shadow.

The big difference with my method compared to Mike Daily's is mine is calculated in the GPU. I will need to only create one common shadow vertex buffer for all lights instead of creating unique vertex buffers for every light. this common vertex buffer will be reconfigured differently through the shaders by changing the light position passed through the shader.

Note that we are drawing an invisible shadow (fragment shader code) but don't worry, this shadow will be used to block its corresponding light without affecting the previous lights, that's how we can draw all the lights directly without needing to draw on a separate surface. But in order to do this, we will need to utilize the Z-buffer to handle the depth.

Before we go to the draw function, let us first setup the shader uniform variables in the setup obejct create:
GML:
//Shader uniform variable setup
u_pos = shader_get_uniform(shd_light,"u_pos");
u_pos2 = shader_get_uniform(shd_shadow,"u_pos");
u_z = shader_get_uniform(shd_light,"u_z");
u_z2 = shader_get_uniform(shd_shadow,"u_z");
Here is the draw function of the setup object which draws the shadows first, then the light.
GML:
//Local variables setup
var _u_pos = u_pos;
var _u_pos2 = u_pos2;
var _u_z = u_z;
var _u_z2 = u_z2;
var _vb = vb;

//Turn on the Zbuffer (3D)
gpu_set_ztestenable(1);
gpu_set_zwriteenable(1);
var _z = 0;
with(obj_light){

    //Draw the shadows (AKA light blockers)
    shader_set(shd_shadow);
    shader_set_uniform_f(_u_pos2,x,y);
    shader_set_uniform_f(_u_z2,_z);
    vertex_submit(_vb,pr_trianglelist,-1);

    //Draw the Light
    gpu_set_blendmode(bm_add);
    shader_set(shd_light);
    shader_set_uniform_f(_u_pos,x,y);
    shader_set_uniform_f(_u_z,_z);
    draw_rectangle(0,0,320,180,0); //canvas for drawing the light
    gpu_set_blendmode(bm_normal);

    _z--; //Next set of shadows and lights is set closer to the screen
}
shader_reset();
gpu_set_ztestenable(0);
gpu_set_zwriteenable(0);
we turn on ztestenable and zwriteenable to allow the use of Z-buffer depth handling. Remember the u_z variable in the light shader? it is also present in the shadow shader. the only difference is that the shadow sets its z coordinate to u_z -0.5, while the light sets its shadow to just u_z. This lets the shadow be drawn over the light, even if the shadow is drawn first. The cool thing is, even if the shadow is transparent, the light will not be able to draw over the shadow. The lights previously drawn that are under the shadow will not be affected since the shadow is transparent. after drawing each light and shadow pair, we bring the _z value closer to the screen so the next light will be over the previous shadows.

Lastly, remember that the light should be drawn with additive blend since that's how lights work. We don't want the lights to replace other lights but rather merge with other lights. The rectangle is set to be the view width and height. Right now, it is simply set at (0,0,320,180) but this should be changed depending on the view dimensions and position.

And that's it! Although we used several advanced concepts like shaders and vertex buffers, the code is relatively simple, yet effective. We can further improve this implementation with post-processing. I was able to implement wall edge lighting which I plan on sharing some other time :) Understand that this system works best with 90 degree edges. It is possible to do this with other polygons or rotated blocks but there will need to be a few tweaks to the code. This system unfortunately cannot handle irregular shaped walls.
Hi GrizzliusMaximus,

I was wondering when part 6 is coming out and if you are okay because we haven't seen you post in 5 months
 
Hi GrizzliusMaximus,

I was wondering when part 6 is coming out and if you are okay because we haven't seen you post in 5 months
Hello Radonred,

Some good news. I've been recently working on Part 6, or the new refined lighting system with edge lighting. The reason for not making part 6 sooner was because the edge lighting I implemented before was very buggy and I did not want to release something that would cause more problems. It was also very hard to implement and hard to explain in a video. I had made many attempts to come up with a better solution, but only recently have I come up with something worth sharing.

I'm still modifying the current implementation so it might still take a while until I release the video.

As for my well-being. I am very much doing fine. When I first made my lighting tutorial, I was still in university. My life got heavy with more important stuff. Now, I graduated and have a fulltime job. Aside from being busy, there were other hobbies that I have been doing that was not game dev related.

I'm hoping to get back to my YouTube channel and making games. I'll try to also be more active in social media. I have to admit I've been ignoring all of you. sorry about that.
 

SushiPop

Member
is there a way to use this with tiles instead of wall objects?
It wholly depends on if you have a way to get vertex information for your tiles into the vertex buffer. It also depends on if you want the shadows to be accurate with the tiles, or just squares and triangles like in the tutorials.

The first way I would approach it is by iterating through the tilemap data for a given layer. for each point in that data that has a tile, you can draw a quad like in the tutorial. You would find the tile position by multiplying your tiles width or height by the X or Y position you are reading from the tilemap.

The issue here is that for triangular tiles, you would need to determine which tile indexes in your tileset are slopes, and this is something that would need to be hard coded unless you keep your tilesets identical across each and every one.

TL;DR, i think doing this with tiles is just more tedious than its worth. I dont think there is anything wrong with using objects to map out collision areas, and if you're already using tile based collisions, you could use objects for shadow casting, and i honestly think it would save you alot of trouble.

EDIT: I forgot to mention that if you do want to use the tilemap approach, you would want to optimize it by being able to cut your tilemap layer into chunks. Of course if you have a big rectangular region made up of 32x32 tiles, you wouldnt want to draw an individual quad for each one. You would want to draw a quad for the whole region. Thats why I think objects are just easier for this because its probably going to be a hell of alot easier to MAYBE deal with instance performance issues down the line, as opposed to figuring out how to shadow cast a tile layer in a performant way.
 

null301

Member
It wholly depends on if you have a way to get vertex information for your tiles into the vertex buffer. It also depends on if you want the shadows to be accurate with the tiles, or just squares and triangles like in the tutorials.

The first way I would approach it is by iterating through the tilemap data for a given layer. for each point in that data that has a tile, you can draw a quad like in the tutorial. You would find the tile position by multiplying your tiles width or height by the X or Y position you are reading from the tilemap.

The issue here is that for triangular tiles, you would need to determine which tile indexes in your tileset are slopes, and this is something that would need to be hard coded unless you keep your tilesets identical across each and every one.

TL;DR, i think doing this with tiles is just more tedious than its worth. I dont think there is anything wrong with using objects to map out collision areas, and if you're already using tile based collisions, you could use objects for shadow casting, and i honestly think it would save you alot of trouble.

EDIT: I forgot to mention that if you do want to use the tilemap approach, you would want to optimize it by being able to cut your tilemap layer into chunks. Of course if you have a big rectangular region made up of 32x32 tiles, you wouldnt want to draw an individual quad for each one. You would want to draw a quad for the whole region. Thats why I think objects are just easier for this because its probably going to be a hell of alot easier to MAYBE deal with instance performance issues down the line, as opposed to figuring out how to shadow cast a tile layer in a performant way.
well I guess you're right, since the only reason I wanted to do it with tiles was because I was paranoid about the performance but now I'll just use objects and disable them when out of view, thanks for your reply :)
 

SushiPop

Member
well I guess you're right, since the only reason I wanted to do it with tiles was because I was paranoid about the performance but now I'll just use objects and disable them when out of view, thanks for your reply :)
If your objects arent doing anything, the performance hit is going to be negligible. I hope it works for you!
 

Wigglebot37

Member
Hello Radonred,

Some good news. I've been recently working on Part 6, or the new refined lighting system with edge lighting. The reason for not making part 6 sooner was because the edge lighting I implemented before was very buggy and I did not want to release something that would cause more problems. It was also very hard to implement and hard to explain in a video. I had made many attempts to come up with a better solution, but only recently have I come up with something worth sharing.

I'm still modifying the current implementation so it might still take a while until I release the video.

As for my well-being. I am very much doing fine. When I first made my lighting tutorial, I was still in university. My life got heavy with more important stuff. Now, I graduated and have a fulltime job. Aside from being busy, there were other hobbies that I have been doing that was not game dev related.

I'm hoping to get back to my YouTube channel and making games. I'll try to also be more active in social media. I have to admit I've been ignoring all of you. sorry about that.
Can't wait to see it come to fruition šŸ˜
 
Top