[Solved] Need help on a raycasting light shader

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
User avatar
Bigfoot71
Party member
Posts: 287
Joined: Fri Mar 11, 2022 11:07 am

[Solved] Need help on a raycasting light shader

Post by Bigfoot71 »

Hello and happy new year everyone :D

I'm coming to you because I'm totally stuck on a shader I'm trying to do.

Basically I need a shader which diffuses light according to a given position (the light is in fact only the normal color of the screen which darkens according to the distance compared to the radius).
That I managed to do, but comes the moment of collisions with the walls and I never manage to have the expected result, either the light stops or continues its path a bit anyhow, or it doesn't. there is no more light at all...

So I totally rewrote this shader several times, removed some useless parts in the collision to read it better, etc. but still the same problems... (the wall collisions is actually a canvas containing white pixels for empty areas and black pixels for obstacles)

If anyone has done this before and could point me in the right direction or tell me where I'm wrong that would be great!
(Knowing that I'm still new to shaders, maybe I don't know some things that would be useful to me in this case)

Here is where I am currently (simplified version and comment)

Code: Select all

extern number light_radius;
extern vec2 light_pos;
extern Image collision_mask;

vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {

    vec4 pixel = Texel(texture, texture_coords);

    // Calculation of the distance between the position of the light and the current position on the screen

    number distance = length(light_pos - screen_coords);

    // If the distance is greater than the radius of the light, do not display light

    if (distance > light_radius) {
        pixel.a = 0.2;
        return pixel;
    }

    // Start raycasting from the current position on the screen

    vec2 ray_direction = normalize(light_pos - screen_coords);
    vec2 current_position = screen_coords;
    
    // Repeat until the ray reaches the position of the light or encounters an obstacle

    while (current_position != light_pos) {

        current_position += ray_direction;    // Move ray
        
        // Check if there is an obstacle at this location in the collision mask

        if (Texel(collision_mask, current_position).r == 0) {
            break; // S'il y a un obstacle, arrêter de raycaster
        }

    }
    
    // If the ray reaches the position of the light, show the normal color

    if (current_position == light_pos) {
        return Texel(texture, texture_coords) * color;
    }

    // Otherwise, display a dark pixel

    pixel.a = 0.2;
    return pixel;

} 
This version simply does not display light...
Last edited by Bigfoot71 on Sat Jan 07, 2023 4:12 pm, edited 1 time in total.
My avatar code for the curious :D V1, V2, V3.
User avatar
darkfrei
Party member
Posts: 1204
Joined: Sat Feb 08, 2020 11:09 pm

Re: Need help on a raycasting light shader

Post by darkfrei »

I cannot help with shader, but it's possible to solve it without them (by using meshes):
viewtopic.php?p=244845#p244845
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
User avatar
Bigfoot71
Party member
Posts: 287
Joined: Fri Mar 11, 2022 11:07 am

Re: Need help on a raycasting light shader

Post by Bigfoot71 »

darkfrei wrote: Thu Jan 05, 2023 2:40 pm I cannot help with shader, but it's possible to solve it without them (by using meshes):
viewtopic.php?p=244845#p244845
Thank you for the link, it could be useful for other things, the current problem I have is that the light is made to be diffused in a labyrinth where there would be a lot of collisions and therefore polygons to add.

I've already tried without shader in several ways but the number of polygons is way too big (even reducing them to the maximum, simplifying them, etc.) so the loading time is very long and it was also very slow to run, hence my idea to make it a shader that uses a collision mask, which is already much faster to generate and surely more efficient.

Here's an example of the game I'm trying to make (with a broken light effect obviously)
EB9LXpM.png
EB9LXpM.png (48.46 KiB) Viewed 1003 times
My avatar code for the curious :D V1, V2, V3.
User avatar
darkfrei
Party member
Posts: 1204
Joined: Sat Feb 08, 2020 11:09 pm

Re: Need help on a raycasting light shader

Post by darkfrei »

I am pretty sure that you need to have the tiles that you see or that you can see, for example just the green tiles from the pink area:
image-01.png
image-01.png (48.42 KiB) Viewed 2144 times


Or just add to the raycasting system just optimized tiles from tile bazed FOV viewtopic.php?p=241909#p241909
Image
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
User avatar
Bigfoot71
Party member
Posts: 287
Joined: Fri Mar 11, 2022 11:07 am

Re: Need help on a raycasting light shader

Post by Bigfoot71 »

I had forgotten to specify it, I have already tried by sending only a portion of the map around the character using the Lighter module, however with each movement I lost more than 40 FPS at once (according to tests that I have just redone at the moment it finally comes from the method :addPolygon() of the module which is very slow).

I tried to reproduce it, here is an image and the function (which I just quickly rewrote) to get this portion:
aKq2Yl1.png
aKq2Yl1.png (10.74 KiB) Viewed 1002 times

Code: Select all

sendSquareAreaToLighter = function(self, size)

        local map = self.map
        local ax = self.player.ax
        local ay = self.player.ay

        local indexes = {}

        for row = ax - size, ax + size do
            for col = ay - size, ay + size do

                if row > 0 and row < #map and map[row][col] == 10 then
                    table.insert(indexes, {row, col})
                end

            end
        end

        for i = #self.light_collisions, 1, -1 do
            self.lighter:removePolygon(self.light_collisions[i])
            table.remove(self.light_collisions, i)
        end

        for i = 1, #indexes do

            local v = indexes[i]
            local x, y = self:mapCoordsToWorld(v[1],v[2])

            self.light_collisions[i] = {
                x, y, x+self.cell_size, y,
                x+self.cell_size, y+self.cell_size,
                x, y+self.cell_size
            }

            self.lighter:addPolygon(
                self.light_collisions[i]
            )

        end

    end;
My avatar code for the curious :D V1, V2, V3.
User avatar
darkfrei
Party member
Posts: 1204
Joined: Sat Feb 08, 2020 11:09 pm

Re: Need help on a raycasting light shader

Post by darkfrei »

See also this shadows thread:
viewtopic.php?p=250910#p250910
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
User avatar
Bigfoot71
Party member
Posts: 287
Joined: Fri Mar 11, 2022 11:07 am

Re: Need help on a raycasting light shader

Post by Bigfoot71 »

I tried to optimize the function I showed above by only loading the new squares in the area and unloading those that are no longer there but it's still slow, I have a loss of 3~4 FPS each time the function is called, so each move (which is much better but still not acceptable)...

Here is the function optimized as I could:

Code: Select all

getCurrentCollisions = function (self)

        local ax, ay = self.player.ax, self.player.ay
        local chunk = self.chunk

        local map = self.map
        local polygons = self.polygons
        local chunk_size = self.chunk_size
        local cell_size = self.cell_size

        local new_elements = {} -- Create a temporary table to store new items

       -- Addition of new elements in the temporary table

        for x = ax - chunk_size, ax + chunk_size do
            for y = ay - chunk_size, ay + chunk_size do

                -- Check that the indexes are valid (within the limits of the maze)

                if x >= 1 and x <= #map and y >= 1 and y <= #map[1] then

                    -- Check that the current index is not the starting index and that it is a wall

                    if not (x == ax and y == ay) and map[x][y] == 10 then
                        table.insert(new_elements, {x, y})
                    end

                end
            end
        end

        -- Traverse the results table and delete elements that are no longer in the zone

        for i = #chunk, 1, -1 do

            local found = false
            for _, v in ipairs(new_elements) do
                if v[1] == chunk[i][1] and v[2] == chunk[i][2] then
                    found = true
                    break
                end
            end

            if not found then -- Remove the current element from the result table
                table.remove(chunk, i)
                self.lighter:removePolygon(polygons[i])
                table.remove(polygons, i)
            end

        end

        -- Add new items to the results table

        for _, v in ipairs(new_elements) do

            -- Check that the current index is not already present in the result table

            local found = false
            for _, w in ipairs(chunk) do
                if v[1] == w[1] and v[2] == w[2] then
                    found = true
                    break
                end
            end

            if not found then -- Add the current index to the result table

                table.insert(chunk, v)

                local px, py = self.world
                    :mapCoordsToWorld(v[1], v[2])

                local new_poly = {
                    px, py,
                    px + cell_size, py,
                    px + cell_size, py + cell_size,
                    px, py + cell_size
                }

                table.insert(polygons, new_poly)
                self.lighter:addPolygon(new_poly)

            end

        end

    end;
Thank you for helping me I will check this new link :)
My avatar code for the curious :D V1, V2, V3.
Andlac028
Party member
Posts: 174
Joined: Fri Dec 14, 2018 2:27 pm
Location: Slovakia

Re: Need help on a raycasting light shader

Post by Andlac028 »

Make sure, you check only polygons in possible light radius. Also if you move only by one tile, you can make sure that at the beginning, you make add polygons from square with size of 2x radius of light and center of player and then just if player moves right, remove first collumn and add next collumn of polygons.
Also it seems like addPolygon updates light at each call, which can be expensive, so maybe try to simplify polygons or look in the lib, if light could be updated only after all changes to polygon, not on each change.
User avatar
darkfrei
Party member
Posts: 1204
Joined: Sat Feb 08, 2020 11:09 pm

Re: Need help on a raycasting light shader

Post by darkfrei »

How about to convert tiles to rectangles/polygons?
2023-01-06T09_29_38.png
2023-01-06T09_29_38.png (5.19 KiB) Viewed 2034 times
2023-01-06T09_29_45.png
2023-01-06T09_29_45.png (3.65 KiB) Viewed 2034 times

Code: Select all

map = {
	{1,1,1,1,1,1,1,1,1,1},
	{1,0,0,1,0,1,0,0,0,1},
	{1,1,0,1,0,0,0,1,0,1},
	{1,0,0,0,0,1,1,1,0,1},
	{1,0,1,1,0,1,0,1,0,1},
	{1,0,0,1,0,1,0,1,0,1},
	{1,0,1,1,0,1,0,1,0,1},
	{1,0,0,1,0,1,0,0,0,1},
	{1,1,1,1,1,1,1,1,1,1},
}

Code: Select all

rectangles = {}
local firstX, lastX = 1, 10
local firstY, lastY = 1, 9
for y = firstY, lastY do
	local r
	for x = firstX, lastX do
		if map[y][x] == 1 and (x == lastX) then -- lastwall
			if r then
				r.w=r.w+1
			else
				r = {x=x,y=y,w=1,h=1}
			end
			table.insert (rectangles, r)
		elseif map[y][x] == 1 then -- wall
			if r then
				r.w=r.w+1
			else
				r = {x=x,y=y,w=1,h=1}
			end
		elseif r then -- not wall, save existing rectangle
			table.insert (rectangles, r)
			r = nil
		end
	end
end

Code: Select all

function love.draw()
	love.graphics.scale(32)
	love.graphics.setLineWidth(1/32)
	for i, r in ipairs (rectangles) do
		love.graphics.rectangle('line', r.x, r.y, r.w, r.h)
	end
end
Also after vertical optimizing (I have no lua code for it):
2023-01-06T09_29_46.png
2023-01-06T09_29_46.png (1.82 KiB) Viewed 2032 times
Also you can hold such structure in the memory and don't make it on every update.
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
User avatar
Sasha264
Party member
Posts: 131
Joined: Mon Sep 08, 2014 7:57 am

Re: Need help on a raycasting light shader

Post by Sasha264 »

Hi! Happy new year :3
There is just the shader, full example in the attachment:

Code: Select all

extern float light_radius;
extern vec2 light_pos;
extern float ambient;
extern vec2 size;
extern float wall_height;
extern Image collision_mask;

vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {

    vec4 pixel = Texel(texture, texture_coords);

    float dist = length(light_pos - screen_coords);

    if (dist > light_radius) {
        // that way I have actually rgb color multiplied, leaving alpha component untouched
        return vec4(pixel.rgb * ambient, 1.0);
    }

    // max number of ray steps
    float togo = floor(dist);

    // aproximatelly has length == 1.0, but not exactly. With this approach ray will arrive to light_pos way more accurately
    vec2 ray_step = (light_pos - screen_coords) / togo;
    // if instead we will do this there will be artifacts when light source close to a corner
    // vec2 ray_step = normalize(light_pos - screen_coords);

    vec2 current_position = screen_coords;

    // Original while (current_position != light_pos) is bad, because current_position will never be exactly == light_pos.
    // Just because they are floats. We can never compare floats with == and != only > < (or something like distance(a, b) < 0.001 if a and b is vec2, vec3)
    // Well, strictly speaking we can sometimes if floats actually are integers, like 0.0 or 2.0, but this was not the case.
    for (float passed = 0.0; passed < togo; passed += max(1.0, 1.0 / wall_height)) {
        // current_position changing inside (0..w-1, 0..h-1) but to access texture we need to normalize it to (0..1, 0..1), so need to divide by texture size
        if (Texel(collision_mask, current_position / size).r == 0.0) {
            return vec4(pixel.rgb * ambient, 1.0);
        }

        // it is better to check mask first, step later, not the other way around, fixes some wrongly enlighted pixels for us
        current_position += ray_step;
    }

    // I don't know how original shader managed to do light distance fade, so here I wrote some smoothstep for that purpose
    float light = ambient + (1.0 - ambient) * smoothstep(light_radius, 0.0, dist);
    return vec4(pixel.rgb * light, 1.0);
}
Attachments
LightLabyrinth_v2.love
(42.4 KiB) Downloaded 81 times
screens
screens
result.png (70.51 KiB) Viewed 1986 times
Last edited by Sasha264 on Sat Jan 07, 2023 2:26 pm, edited 1 time in total.
Post Reply

Who is online

Users browsing this forum: Amazon [Bot], Google [Bot] and 11 guests