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.
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.
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.
Comments
Post a Comment