Glusoft

Creating a 2D light with SDL

The goal is creating a 2D light with SDL, something like this video:

Setting up of creating a 2D light

For this tutorial you will need to have these extensions:

Map and walls

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

loading="lazy" decoding="async" src="map.png" alt="Top down map hotline miami for creating a 2D light">

In this map the walls can be represented by segments:

walls of the map for creating a 2D light with SDL

Initialization and loading

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.

Event loop and visibility polygon

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());
		}
	}

Rendering of creating a 2D light

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