Dynamic Shadow

Hey there! Today I’m gonna show you how to make dynamic shadows in GameMaker:Studio. The code is taken from a post by flokkienathur on the old GameMaker forums. I changed quite a bit of the code. The basic concept is this: we will have a “light” object and a “caster” object. When the caster object passes in front of the light, it will create a shadow. This will be controlled through scripts and a “light map” object. Again I have to give credit to flokkienathur for most of this code. This tutorial will be a bit confusing for beginners. You will need to understand how surfaces and blending work. Also there is one major bug (explanation at the end).
SETUP
First off let’s create some objects. Create one called oParCaster and oParLight. These will be the parent object for our effects. Also create an object called oLightMap. This object will store our surface for our light map.
Lightmap scripts
Next we will create some scripts for the light map.

scrLightMapInit:

///scrLightMapInit(width, height, parCaster, parLight) //Use width and height of room as long as the room is somewhat small lm_width = argument0; //width of lightmap (usually width of room) lm_height = argument1; //height of lightmap (usually height of room) lm_surface_id = surface_create(lm_width, lm_height); //Store the id of the surface so we can access it //Parents of casters and lights lm_parent_caster = argument2; lm_parent_light = argument3;
We will call this script in the create event of our lightmap object. Like this: scrLightMapInit(room_width, room_height, oParCaster, oParLight); The next script will update the light map (put it in the step event of the light map object). If we didn’t update the light map then we would just have static shadows (ones that don’t move). This can be what you want if you want to “bake” in the shadows. In that case you would only call this script once.

scrLightMapUpdate:
///scrLightMapUpdate() //check if the lightmap surface exists if(!surface_exists(lm_surface_id)){ //if it does not exist, recreate it lm_surface_id = surface_create(lm_width,lm_height); } surface_set_target(lm_surface_id); //do all the next events to our light map draw_clear(make_colour_rgb(0,0,0)); //clear the surface var lmid = id; //lightmap object id if (lm_parent_light != -1) { //check that there is a parent object for the light with (lm_parent_light) { //do the next events for all lights that exist var lx = x; //light x pos var ly = y; //light y pos var lr = light_radius; //light radius (set in the light object) var lid = id; //light object id with(lmid.lm_parent_caster) { //get the caster parent (from the lightmap) if point_in_circle(x,y,lx,ly,lr) { //check if the caster is in the light’s radius //start drawing shadow draw_primitive_begin(pr_trianglestrip); draw_set_color(c_white); //draw shadow primitive from each point on caster for(i = 0; i < caster_point_count-1; i++){ scrLightMapDrawShadow(id, lid, lr, caster_point_x[i], caster_point_y[i], caster_point_x[i+1], caster_point_y[i+1]); } //close the shape from the last point of the caster to the first point scrLightMapDrawShadow(id, lid, lr, caster_point_x[caster_point_count-1], caster_point_y[caster_point_count-1], caster_point_x[0], caster_point_y[0]); draw_primitive_end(); //stop drawing shadow } } } } //reset this light surface surface_reset_target();
I’m gonna explain how the shadows are drawn in a minute (there are some bugs still that I’m working out). Don’t run anything yet we still have a ways to go. Call this next script in the draw event of the light map object. It simply draws our surface will blending.
scrLightMapDraw:
///scrLightMapDraw() draw_set_blend_mode(bm_subtract); draw_surface(lm_surface_id,0,0); draw_set_blend_mode(bm_normal);
Lights: Put this next script in the create event of you light object (YOUR light object not the parent light object). Make sure to set the parent as the parent light object!
scrLightInit:
///scrLightInit(radius) light_radius = argument0;
Pretty simple. The original light engine by flokkienathur had support for other things such as sprite lights and other fancy stuff. Since we are making a shadow engine though our lights aren’t actually doing anything other than telling the game where we want shadows to be. Casters: Casters are going to be our “light blockers” so to speak. We will set up point in an array that store their shape. We then use this shape to cast shadows. Like with the lights, simply add these scripts to your caster objects (walls, player, etc.) and make the caster parent the parent.
scrCasterInitPolygon:
///scrCasterInitPolygon() caster_point_count = 0; scrCasterAddPoint: ///scrCasterAddPoint(x, y) caster_point_x[caster_point_count] = argument0; caster_point_y[caster_point_count] = argument1; caster_point_count += 1;
These next scripts are for specifying what shape. You could always make your own shapes by using the previous two scripts. These are just easy presets.
scrCasterInitCircle:
scrCasterInitCircle(radius, fractions) scrCasterInitPolygon(); var radius = argument0; var fractions = argument1; //how accurate you want the circle to be var i; var m; m = 2 * 3.141592654 / fractions; for(i = 0; i < fractions; i++){ scrCasterAddPoint(cos(m * i) * radius, sin(m * i) * radius); }
scrCasterInitRectangle:
//scrCasterInitRectangle(left, top, bottom, right) scrCasterInitPolygon(); var left = argument0; var top = argument1; var right = argument2; var bottom = argument3; scrCasterAddPoint(left,top); scrCasterAddPoint(right,top); scrCasterAddPoint (right,bottom); scrCasterAddPoint (left,bottom);
scrCasterInitSprite:
///scrCasterInitSprite() scrCasterInitPolygon(); scrCasterAddPoint (-sprite_xoffset,-sprite_yoffset); scrCasterAddPoint (-sprite_xoffset + sprite_width,-sprite_yoffset); scrCasterAddPoint (-sprite_xoffset + sprite_width,-sprite_yoffset + sprite_height); scrCasterAddPoint (-sprite_xoffset,-sprite_yoffset + sprite_height);
Great the caster scripts are all setup! Put one of the shape scripts in the create event of your caster object and set the parent as the caster parent object and there you go! Next we will actually set up the drawing script. This one requires more explanation so I left it for last.
scrLightMapDrawShadow:
///scrLightMapDrawShadow(caster, light, lightRadius, x1, y1, x2, y2) var caster = argument0; //caster id var light = argument1; //light id var lightRadius = argument2; //radius //caster point are relative to the caster, thus we must add the caster’s position to compensate and get the actual position in the world var Ax = argument3 + caster.x; //startx var Ay = argument4 + caster.y; //starty var Bx = argument5 + caster.x; //endx var By = argument6 + caster.y; //endy var Lx = light.x; //light x var Ly = light.y; //light y //angles to calculate end points (these are just from the light to the caster points) var shadowAngleA = point_direction(Lx, Ly, Ax, Ay); var shadowAngleB = point_direction(Lx, Ly, Bx, By); //Next we calculate the end points of the shadow //The shadow will be drawn from our caster points to the end of the light’s radius var E1x, E1y, E2x, E2y; E1x = Lx+lengthdir_x(lightRadius, shadowAngleA); E1y = Ly+lengthdir_y(lightRadius, shadowAngleA); E2x = Lx+lengthdir_x(lightRadius, shadowAngleB); E2y = Ly+lengthdir_y(lightRadius, shadowAngleB); //We will fade the shadow out as it gets to the end of the light radius so that it will look better var c = c_white; var alphaEnd = 0; var dist = point_distance(caster.x, caster.y, Lx, Ly); if dist == 0 dist = 1; var lightToCaster = point_direction(Lx,Ly, caster.x, caster.y); var totalDist = point_distance(Lx,Ly, Lx+lengthdir_x(lightRadius,lightToCaster), Ly+lengthdir_y(lightRadius,lightToCaster)); //This is our start alpha, it will be darker if the caster is closer to the light var alphaStart = 0.25*(1-(dist/totalDist)); //We now add vertexes to our shadow primitive draw_vertex_colour(Ax, Ay,c,alphaStart); //start of point a draw_vertex_colour(E1x, E1y,c,alphaEnd); //end of point a draw_vertex_colour(Bx, By, c, alphaStart); //start of point b draw_vertex_colour(E2x, E2y, c, alphaEnd); //end of point b
Since this script is long I have provided a nice graphic to illustrate what is going on.
Basically we are drawing a trianglestrip primitive for the shadow. We add vertexes from one point on the caster to and end point back to another point on the caster and back to an end point. We repeat this for all the points on the caster in the lightmap update script.

The main glitch here is that one shadow can be drawn over another shadow which messes up our nice effect. I have yet to figure out how to fix this. My idea would be to test that the end points of the shadow don’t run into another caster. This should prevent shadows from drawing on top of each other. I will post an update when I fix this.

Comments

Popular Posts