A Thorough Tutorial
By: Austin Shindlecker (Austech)
Sections (in order)
Recommended Knowlege
Recommended Sites
What we'll cover and how
What makes a light dynamic
SFML Objects You Should be Aware of
Coding The Lights and Blocks
Making The Engine
Recommended Knowledge Needed:
Some understanding of the Trigonomic Functions (cos, sin, tan)
Pythagorean Theorem
Well enough experience in the language you plan on using (duh)
----------------------------------------------
Reccomended Sites if you don't want to read this thread:
http://www.gamedev.net/reference/progra ... oftshadow/
----------------------------------------------
What We'll Cover and how
The code you will see is made in C++ and using the SFML library, but I'm pretty sure that you will be able to get the basic idea of what to do\
This tutorial will only be discussing what is called "hard shadows". The shadows do not blend or fade. But they are dynamic
-----------------------------------------------
What makes a light dynamic
So what makes a light dyamic? First off dynamic, when compared to static, is something that changes. While static is something that doesn't. For those who are still confused here's an example. In my game, Zombie Outrage 2, the lighting system I made has a boolean for whether or not a light is dynamic.
Static Light:
Dynamic Light:
Notice how with dynamic the light stops when it hits the zombies or player, creating a field of darkness behind them. Yet with the static it goes through. With dynamic the light changes.
------------------------------------------------
SFML Objects You Should be Aware of
As I said, I'm going to be using SFML (Simple, Fast, Multimedia Library), and I will be using some objects.
sf::Vector2f - Vector holding 2 floats (x,y)
The structure looks something like:
Code: Select all
struct Vector2f
{
float x, y;
};
sf::FloatRect - Rectangle object holding 4 floats. (Left, Top, Width, Height).
The structure looks something like:
Code: Select all
struct FloatRect
{
float Left, Top, Width, Height;
};
sf::Color - Object holding values for Red, Green, Blue, and Alpha. This is used only for rendering, nothing based on the actual lighting system
The structure looks something like:
Code: Select all
struct Color
{
int r, g, b, a;
};
-------------------------------------------
Coding the Lights and Blocks
So now that the intro is done and some things are explained. Let's get into the code!
We will be making 3 classes:
Light - Info for lighting (color, radius, position, spread)
Block - Areas where the light can't go through (rect, allowblock)
LightEngine - This is the class that does what is needed with the Lights and Blocks.
Let's get the first 2 classes out of the way since they are the easiest. Since it would be a pain to write a tutorial will .cpp and .hpp, I'm going to just right all the bodies in the .hpp file
Code: Select all
//Light.hpp
#pragma once //don't allow this header to be included more than once
#include <SFML/Graphics/Color.hpp>
#include <SFML/System/Vector2.hpp>
class Light
{
public:
Light()
{
color = sf::Color(255, 255, 255);
radius = 0;
angleSpread = 0;
position = sf::Vector2f(0,0);
angle = 0;
dynamic = true;
}
/*If you prefer, you can do the constructor like so:
Light()
: color(255, 255, 255), radius(0), angleSpread(0), position(0,0), angle(0), dynamic(true)
{
}
*/
sf::Vector2f position; //position of the light
sf::Color color; //light color
float radius; //how far the light will shine to
float angleSpread; //spread of the light, creating an arc
float angle; //where the light's pointing at
bool dynamic; //can this change?
};
Code: Select all
//Block.hpp
#pragma once//don't allow this header to be included more than once
#include <SFML/Graphics/Rect.hpp>
class Block
{
public:
Block()
{
allowBlock = true;
fRect = sf::FloatRect(0,0,0,0);
}
/* Like the light class, you can do this in an initializer list, whatever works for you
Block()
: fRect(0,0,0,0), allowBlock(false)
{
}
*/
sf::FloatRect fRect; //Area that will be blocking light
bool allowBlock; //Sometimes we want to keep a block, but don't want it to actually block until a certain point
};
-----------------------------------------
Making the engine
The engine will be taking these 2 objects we made, and actually do things with them.
The light engine should have some sort of container for holding multiple lights and blocks. I'll be using std::vector.
The light engine should have a private Light() function to do things with a light.
The light engine should have some sort of step function to light each light in a vector.
A useful function to get the distance of two points
A function to check if a light hits a block
Maybe some useful functions to make long equations return an answer neater than typing it call out.
My class looks like this:
Code: Select all
//LightEngine.hpp
#pragma once//don't allow this header to be included more than once
#include "Light.hpp"
#include "Block.hpp"
#include <vector>
#include <SFML/Graphics/RenderTarget.hpp> //Place to draw on
#include <SFML/Graphics/Shape.hpp> //SFML programmable Shapes
class LightEngine
{
public:
void Step(sf::RenderTarget &rt);
std::vector <Light> Lights; //Container for Lights
std::vector <Block> Blocks; //Container for Blocks
private:
void ShineLight(Light &lig, sf::RenderTarget &rt);
static const float Distance(const sf::Vector2f &p1, const sf::Vector2f &p2);
static const sf::Vector2f GetCenter(const sf::FloatRect &fr); //Get the center of a rectangle
struct FindDistance //if a light's radius manages to intersect multiple blocks, we need to find the sortest distance to shorten the light
{
FindDistance();
float shortest;
bool LightHitsBlock(Light &l, Block &b, const float cur_ang, float &reflength);
bool start; //to get the first distance to refer off of
};
FindDistance findDis;
};
struct FindDistance Structure containing info for calculating how much the light ray should be shortened.
FindDistance::LightHitsBlock - One of the main functions in LightEngine. Checks if a light hit's the block. This changes the findDis info.
ShineLight - Another main functions for ray casting a light
Distance - Static functionto find the distance between 2 points. This doesn't HAVE to be in the LighEngine class. It can be anywhere. I will use it for cleaner code and it's easier than typing the calculations over and over.
GetCenter - Get Center of a Rectangle
Now that you have a basic understanding of what each function and structure does. Let's get into the bodies of the structures. Like before, we'll do the easiest to hardest.
Code: Select all
LightEngine::FindDistance::FindDistance()
{
start = false;
shortest = 0;
}
const sf::Vector2f LightEngine::GetCenter(const sf::FloatRect &fr)
{
return sf::Vector2f(fr.Left + (fr.Width / 2), fr.Top + (fr.Height / 2));
}
const float LightEngine::Distance(const sf::Vector2f &p1, const sf::Vector2f &p2)
{
//We need to look at this as a triangle
/*
/|p1.y
/ |
/ |
/ |
/ |
/ |b
/ |
/ |
/ |
/ |
-----------
a p2.y
p1.x p2.x
*/
float a = p2.x - p1.x; //width length
float b = p2.y - p1.y; //height length
float c = sqrt((a * a) + (b * b)); //Pythagorean Theorem. (c² = a² + b²). c = squareroot(a² + b²)
return c;
}
Step Function:
Code: Select all
void LightEngine::Step(sf::RenderTarget &rt)
{
for(unsigned i = 0; i < Lights.size(); i++)
{
ShineLight(Lights[i], rt); //Shine all lights
}
}
Code: Select all
void LightEngine::ShineLight(Light &l, sf::RenderTarget &rt)
{
/*
remember back in the Light class, we had something called 'angleSpread' ?
that's to create a tunnel, or arc shaped light. Like this:
/)
/ )
/ )
/ )
< )
\ )
\ )
\ )
\)
Obviously it'll look better than an ascii drawing
*/
float current_angle = l.angle - (l.angleSpread / 2); //This will rotate the angle back far enough to get a desired arc
/*
Lights Angle (if it was at 0):
-------------
Current_Angle:
/
/
/
(slanted)
*/
float dyn_len = l.radius; //dynamic length of the light. This will be changed in the function LightHitsBlock()
float addto = 1.f / l.radius;
for(current_angle; current_angle < l.angle + (l.angleSpread / 2); current_angle += addto * (180.f/3.14f)) //we need to add to the current angle, until it reaches the end of the arc. we divide 1.f by radius for a more solid shape. Otherwize you could see lines seperating
{
dyn_len = l.radius; //Reset the length
findDis.start = true; //Start of finding a light, we need to reset
findDis.shortest = 0; //Reset the shortest.
if(l.dynamic) //can this change?
{
for(unsigned i = 0; i < Blocks.size(); i++)
{
findDis.LightHitsBlock(l, Blocks[i], current_angle, dyn_len);
}
}
float radians = current_angle * (3.14f / 180); //Convert to radians for trig functions
sf::Vector2f end = l.position;
end.x += cos(radians) * dyn_len;
end.y += sin(radians) * dyn_len;
rt.Draw(sf::Shape(sf::Shape::Line(l.position, end, 1, l.color)));
}
}
current_angle is the direction of where the light ray will go. One light can equal many light rays. Hundreds of them.
Since this function is using the LightHitsBlock function, we're going to go to that and come back.
Code: Select all
bool LightEngine::FindDistance::LightHitsBlock(Light &l, Block &b, float cur_ang, float &refleng)
{
if(b.allowBlock) //can this even block?
{
float distance = Distance(l.position, GetCenter(b.fRect));
if(l.radius >= distance) //check if it's radius is even long enough to hit a block
{
float radians = cur_ang * (3.14f / 180); //convert cur_ang to radians for trig functions
sf::Vector2f pointpos = l.position;
pointpos.x += cos(radians) * distance;
pointpos.y += sin(radians) * distance;
//By doing this, we check if the angle is in the direciton of the block.
if(b.fRect.Contains(pointpos)) //If it was, than the point would be intersecting the rectangle of the block
{
if(start || distance < shortest) //If this is the first block, or it has a shorter distance
{
start = false; //definately not the start so other blocks can't automatically set the distance
shortest = distance; //shortest is set to this
refleng = distance; //This is where the dynamic comes in, it changes the length of the reference towhere it's going to stop after it hits the distance from the point to the block
}
return true;
}
}
}
return false;
}
Check if the Block can block light from passing through
Check if the light's radius can reach to the block
Check if the light's angle direction is in the direction of the block
if it is, then check if the block is the closest to the light
if it is, then change the ray's length to it's distance.
Now back to the ShineLight function:
Code: Select all
void LightEngine::ShineLight(Light &l, sf::RenderTarget &rt)
{
/*
remember back in the Light class, we had something called 'angleSpread' ?
that's to create a tunnel, or arc shaped light. Like this:
/)
/ )
/ )
/ )
< )
\ )
\ )
\ )
\)
Obviously it'll look better than an ascii drawing
*/
float current_angle = l.angle - (l.angleSpread / 2); //This will rotate the angle back far enough to get a desired arc
/*
Lights Angle (if it was at 0):
-------------
Current_Angle:
/
/
/
(slanted)
*/
float dyn_len = l.radius; //dynamic length of the light. This will be changed in the function LightHitsBlock()
float addto = 1.f / l.radius;
for(current_angle; current_angle < l.angle + (l.angleSpread / 2); current_angle += addto * (180.f/3.14f)) //we need to add to the current angle, until it reaches the end of the arc. we divide 1.f by radius for a more solid shape. Otherwize you could see lines seperating
{
dyn_len = l.radius; //Reset the length
findDis.start = true; //Start of finding a light, we need to reset
findDis.shortest = 0; //Reset the shortest.
if(l.dynamic) //can this change?
{
for(unsigned i = 0; i < Blocks.size(); i++)
{
findDis.LightHitsBlock(l, Blocks[i], current_angle, dyn_len);
}
}
float radians = current_angle * (3.14f / 180); //Convert to radians for trig functions
sf::Vector2f end = l.position;
end.x += cos(radians) * dyn_len;
end.y += sin(radians) * dyn_len;
rt.Draw(sf::Shape(sf::Shape::Line(l.position, end, 1, l.color)));
}
}
We pass dyn_len to the LighHitsBlock function as a reference, so the function can change the value of it based off the smallest distance.
After all that, we can finally draw it. We create a line from the lights original position, to the end which is the lights position plus the cos and sin of the dyn_len.
Now we can test it, I set up this simple example to do so:
Code: Select all
#include <SFML/Graphics.hpp>
#include <iostream>
#include "LightEngine.hpp"
int main()
{
sf::RenderWindow win(sf::VideoMode(800, 600), "Light Tutorial");
sf::Event event;
LightEngine le;
Light light;
light.radius = 600;
light.angleSpread = 100;
light.position = sf::Vector2f(100,150);
le.Lights.push_back(light);
Block block;
block.fRect = sf::FloatRect(0,0,50,50);
le.Blocks.push_back(block);
while(win.IsOpened())
{
while(win.GetEvent(event))
{
if(event.Type == sf::Event::Closed) win.Close();
}
std::cout<<1 / win.GetFrameTime()<<"\n"; //output the framerate
win.Clear();
le.Blocks[0].fRect.Left = win.GetInput().GetMouseX();
le.Blocks[0].fRect.Top = win.GetInput().GetMouseY();
win.Draw(sf::Shape::Rectangle(le.Blocks[0].fRect, sf::Color(255,0,0)));
le.Step(win);
win.Display();
}
}
Testing multiple blocks:
Little more
====================================================================
I really hope this helps some people out. I knew that some people wanted to know how my game did it and a way for SFML. Sorry if this wasn't clear enough or easy to read. This is my second tutorial at the moment.
Thanks again.