The goal is creating a 2D light with SDL, something like this video:
For this tutorial you will need to have these extensions:
Before starting to render the 2D light we need to have a top down map with some obstacles to have shadows.
For this example we will use a map of hotline miami: map.png
In this map the walls can be represented by segments:
The first thing to do is creating the renderer and the window, then we can load the texture we need to make the map and the light.
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
IMG_Init(IMG_INIT_PNG);
float width = 1080;
float height = 720;
SDL_Window* window = SDL_CreateWindow("2D Light", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_OPENGL);
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
SDL_Surface* map_sur = IMG_Load("map.png");
SDL_Surface* light_sur = IMG_Load("light.png");
The light for the texture is simply a black and white gradient : light.png
Then we create the textures from the surfaces:
SDL_Rect rectMap{ (width - map_sur->w)/2.f, (height - map_sur->h) / 2.f, map_sur->w, map_sur->h };
SDL_Rect rectLight{ 0, 0, light_sur->w, light_sur->h };
SDL_Texture* map_tex = SDL_CreateTextureFromSurface(renderer, map_sur);
SDL_Texture* light_tex = SDL_CreateTextureFromSurface(renderer, light_sur);
SDL_SetTextureBlendMode(light_tex, SDL_BLENDMODE_ADD);
SDL_Texture *rendertex_light = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, width, height);
SDL_SetTextureBlendMode(rendertex_light, SDL_BLENDMODE_MOD);
SDL_Texture *rendertex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, width, height);
SDL_SetTextureBlendMode(rendertex, SDL_BLENDMODE_MOD);
SDL_FreeSurface(map_sur);
SDL_FreeSurface(light_sur);
As you can see we change the blend mode of the target texture to multiply, because we want to be able to see the map underneath.
The light texture is additive because the texture is a square 720×750 pixels. The windows is larger than that and the texture will move according to the cursor position.
We render the light texture to another render texture filled with black, if we render the texture directly without using a render texture we will see the border of the image.
Then we create the walls with the position of the map:
std::vector<geometry::line_segment<geometry::vec2>> segments;
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 114, rectMap.y + 60), geometry::vec2(rectMap.x + 114, rectMap.y + 372)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 114, rectMap.y + 372), geometry::vec2(rectMap.x + 147, rectMap.y + 372)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 147, rectMap.y + 372), geometry::vec2(rectMap.x + 148, rectMap.y + 382)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 148, rectMap.y + 382), geometry::vec2(rectMap.x + 22, rectMap.y + 383)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 22, rectMap.y + 383), geometry::vec2(rectMap.x + 20, rectMap.y + 648)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 20, rectMap.y + 648), geometry::vec2(rectMap.x + 379, rectMap.y + 648)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 379, rectMap.y + 648), geometry::vec2(rectMap.x + 380, rectMap.y + 464)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 380, rectMap.y + 464), geometry::vec2(rectMap.x + 391, rectMap.y + 464)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 391, rectMap.y + 464), geometry::vec2(rectMap.x + 390, rectMap.y + 509)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 391, rectMap.y + 509), geometry::vec2(rectMap.x + 561, rectMap.y + 513)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 561, rectMap.y + 513), geometry::vec2(rectMap.x + 563, rectMap.y + 521)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 563, rectMap.y + 521), geometry::vec2(rectMap.x + 390, rectMap.y + 521)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 390, rectMap.y + 521), geometry::vec2(rectMap.x + 391, rectMap.y + 647)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 391, rectMap.y + 647), geometry::vec2(rectMap.x + 697, rectMap.y + 649)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 697, rectMap.y + 649), geometry::vec2(rectMap.x + 700, rectMap.y + 523)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 700, rectMap.y + 523), geometry::vec2(rectMap.x + 610, rectMap.y + 520)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 610, rectMap.y + 520), geometry::vec2(rectMap.x + 610, rectMap.y + 511)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 610, rectMap.y + 511), geometry::vec2(rectMap.x + 793, rectMap.y + 513)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 793, rectMap.y + 513), geometry::vec2(rectMap.x + 795, rectMap.y + 387)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 795, rectMap.y + 387), geometry::vec2(rectMap.x + 703, rectMap.y + 387)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 703, rectMap.y + 387), geometry::vec2(rectMap.x + 704, rectMap.y + 250)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 704, rectMap.y + 250), geometry::vec2(rectMap.x + 612, rectMap.y + 247)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 612, rectMap.y + 247), geometry::vec2(rectMap.x + 611, rectMap.y + 238)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 611, rectMap.y + 238), geometry::vec2(rectMap.x + 712, rectMap.y + 239)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 712, rectMap.y + 239), geometry::vec2(rectMap.x + 714, rectMap.y + 378)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 714, rectMap.y + 378), geometry::vec2(rectMap.x + 795, rectMap.y + 375)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 795, rectMap.y + 375), geometry::vec2(rectMap.x + 794, rectMap.y + 156)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 794, rectMap.y + 156), geometry::vec2(rectMap.x + 612, rectMap.y + 156)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 612, rectMap.y + 156), geometry::vec2(rectMap.x + 613, rectMap.y + 64)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 613, rectMap.y + 64), geometry::vec2(rectMap.x + 393, rectMap.y + 63)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 393, rectMap.y + 63), geometry::vec2(rectMap.x + 392, rectMap.y + 98)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 392, rectMap.y + 98), geometry::vec2(rectMap.x + 382, rectMap.y + 97)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 382, rectMap.y + 97), geometry::vec2(rectMap.x + 381, rectMap.y + 62)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 381, rectMap.y + 62), geometry::vec2(rectMap.x + 114, rectMap.y + 60)));
//Wall
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 381, rectMap.y + 145), geometry::vec2(rectMap.x + 381, rectMap.y + 190)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 381, rectMap.y + 190), geometry::vec2(rectMap.x + 288, rectMap.y + 190)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 288, rectMap.y + 190), geometry::vec2(rectMap.x + 287, rectMap.y + 372)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 287, rectMap.y + 372), geometry::vec2(rectMap.x + 195, rectMap.y + 372)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 195, rectMap.y + 372), geometry::vec2(rectMap.x + 195, rectMap.y + 381)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 195, rectMap.y + 381), geometry::vec2(rectMap.x + 298, rectMap.y + 383)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 298, rectMap.y + 383), geometry::vec2(rectMap.x + 299, rectMap.y + 200)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 299, rectMap.y + 200), geometry::vec2(rectMap.x + 390, rectMap.y + 201)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 390, rectMap.y + 201), geometry::vec2(rectMap.x + 391, rectMap.y + 144)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 391, rectMap.y + 144), geometry::vec2(rectMap.x + 381, rectMap.y + 145)));
//Wall
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 473, rectMap.y + 238), geometry::vec2(rectMap.x + 472, rectMap.y + 373)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 472, rectMap.y + 373), geometry::vec2(rectMap.x + 380, rectMap.y + 373)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 380, rectMap.y + 373), geometry::vec2(rectMap.x + 380, rectMap.y + 416)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 380, rectMap.y + 416), geometry::vec2(rectMap.x + 389, rectMap.y + 416)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 389, rectMap.y + 416), geometry::vec2(rectMap.x + 389, rectMap.y + 382)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 389, rectMap.y + 382), geometry::vec2(rectMap.x + 482, rectMap.y + 382)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 482, rectMap.y + 382), geometry::vec2(rectMap.x + 483, rectMap.y + 247)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 483, rectMap.y + 247), geometry::vec2(rectMap.x + 563, rectMap.y + 248)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 563, rectMap.y + 248), geometry::vec2(rectMap.x + 563, rectMap.y + 238)));
segments.push_back(geometry::line_segment<geometry::vec2>(geometry::vec2(rectMap.x + 563, rectMap.y + 238), geometry::vec2(rectMap.x + 473, rectMap.y + 238)));
How do you find the coordinates for the walls ?
I did that with the information tool of photoshop (F8)
Where does the geometry namespace come from ?
To calculate the visibility polygon I use this implementation: visibility
The algorithm need to know the obstacles to be able to raycast efficiently, that is why I use geometry::vec2 and geometry::line_segment.
int cur_posX = 0;
int cur_posY = 0;
rectLight.x = cur_posX - 360;
rectLight.y = cur_posY - 360;
std::vector<geometry::vec2> result = geometry::visibility_polygon(geometry::vec2(cur_posX, cur_posY), segments.begin(), segments.end());
std::vector<Sint16> vx, vy;
SDL_ShowCursor(0);
while (true) {
SDL_Event e;
if (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT) {
break;
}
if (e.type == SDL_MOUSEMOTION) {
cur_posX = e.motion.x;
cur_posY = e.motion.y;
rectLight.x = cur_posX - 360;
rectLight.y = cur_posY - 360;
result.clear();
result = geometry::visibility_polygon(geometry::vec2(cur_posX, cur_posY), segments.begin(), segments.end());
}
}
The first thing to render is the light on the black texture and the visibility polygon with SDL_gfx.
After that we can render the map, then the visibility polygon texture and finally the light texture.
// Reset the default color after filledPolygonRGBA
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
//Render light on the texture
SDL_SetRenderTarget(renderer, rendertex_light);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, light_tex, NULL, &rectLight);
// Render visibility polygon on the texture
SDL_SetRenderTarget(renderer, rendertex);
SDL_RenderClear(renderer);
vx.reserve(result.size());
vy.reserve(result.size());
for (size_t i = 0; i < result.size(); i++) {
vx[i] = result[i].x;
vy[i] = result[i].y;
}
filledPolygonRGBA(renderer, &vx[0], &vy[0], result.size(), 255, 255, 255, 255);
// Render on the screen
SDL_SetRenderTarget(renderer, NULL);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, map_tex, NULL, &rectMap);
SDL_RenderCopy(renderer, rendertex, NULL, NULL);
SDL_RenderCopy(renderer, rendertex_light, NULL, NULL);
SDL_RenderDrawPoint(renderer, cur_posX, cur_posY);
SDL_RenderPresent(renderer);
vx.clear();
vy.clear();
}
After exiting the loop we free the textures, renderer and the window.
SDL_DestroyTexture(rendertex);
SDL_DestroyTexture(rendertex_light);
SDL_DestroyTexture(light_tex);
SDL_DestroyTexture(map_tex);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
IMG_Quit();
SDL_Quit();
return 0;
You can download the project of creating a 2D light with SDL : 2DLight.7z
You find the repo of the project here : 2DLight-SDL