How to blend light colours?

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.
Post Reply
User avatar
Gunroar:Cannon()
Party member
Posts: 1091
Joined: Thu Dec 10, 2020 1:57 am

How to blend light colours?

Post by Gunroar:Cannon() »

So in a simple tile based system where a light source spreads colours to tiles around it in a decreasing intensity, then the tile draws the light depending on how far it is from the source (through multiplying r,g,b by a value that gets smaller the further away it is).

This is fine with one color but when more than one "light" is on I tried to make them "blend" together by multiplying all the light colours on tile.

Code: Select all

function multiplyc(t, color, ...)
    for i = 1, #color do
        t[i] = t[i]*color[i]
    end
end


....
    local color = {1,1,1}
    for i, col in pairs(tile.lightColors) do
        color = multiplyc(color, col)
    end
Though this leads to problems with red dominating. I tried adding the colors but I got problems there to (like white dominating) but I read somewhere that multiplying them is the correct way.

Any thoughts on how to fix?
The risk I took was calculated,
but man, am I bad at math.

-How to be saved and born again :huh:
User avatar
BrotSagtMist
Party member
Posts: 626
Joined: Fri Aug 06, 2021 10:30 pm

Re: How to blend light colours?

Post by BrotSagtMist »

Never seen that method for light in the wild.
I always thought the usual approach in games for light is to have the source punch holes in a mask that is then drawn to darken tiles.
obey
User avatar
Gunroar:Cannon()
Party member
Posts: 1091
Joined: Thu Dec 10, 2020 1:57 am

Re: How to blend light colours?

Post by Gunroar:Cannon() »

Ahh. It's a roguelike so I'm using fov for light. I'm just kinda trying to implement the lighting in brogue for example...
NtcICSF.png
NtcICSF.png (178.01 KiB) Viewed 2436 times
Image
The risk I took was calculated,
but man, am I bad at math.

-How to be saved and born again :huh:
User avatar
Bigfoot71
Party member
Posts: 287
Joined: Fri Mar 11, 2022 11:07 am

Re: How to blend light colours?

Post by Bigfoot71 »

Super cool ! :D (I'm so beat I thought it was your job, I really need to rest, what an idiot I am, at least I will have discovered a cool little game :ultrahappy: )

Otherwise by simply adding the colors the problem you describe does not seem to appear but I do not know if this is exactly what you want. Here is the result:
Image

And here is the code I wrote quickly to do it:

Code: Select all

-- Initialize light colors --

local lightColors = {

    { -- yellow (player)
        x = 1, y=1,
        r=1, g=1, b=0
    };

    { -- red
        x = 5, y=5,
        r=1, g=0, b=0
    },

    { -- green
        x = 15, y=10,
        r=0, g=1, b=0
    },

    { -- blue
        x = 10, y=15,
        r=0, g=0, b=1
    },

}

-- Initialize the card --

local mapWidth = 20
local mapHeight = 20
local tileWidth = 32
local tileHeight = 32

local map = {}
for x = 1, mapWidth do
    map[x] = {}
    for y = 1, mapHeight do
        map[x][y] = {0,0,0}
    end
end

-- Main functions --

local function addc(c1, c2)
    for i = 1, #c2 do
        c1[i] = c1[i]+c2[i]
    end
end

--local function normalizec(c)
--    local sum = c[1]+c[2]+c[3]
--    for i = 1, #c do c[i] = c[i]/sum end
--end

local function drawMap()

    for x = 1, mapWidth do
        for y = 1, mapHeight do

            local color = {0, 0, 0} -- Calculate the final tile color by adding the light colors together

            for i, lc in ipairs(lightColors) do
                local distance = math.sqrt((x-lc.x)^2+(y-lc.y)^2)
                local intensity = 1 / (distance + 1)
                addc(color, {lc.r * intensity, lc.g * intensity, lc.b * intensity})
            end

            addc(color, map[x][y]) -- Add the base color of the tile

            love.graphics.setColor(color)
            love.graphics.rectangle("fill", (x-1)*tileWidth, (y-1)*tileHeight, tileWidth, tileHeight)

        end
    end
end

function love.keypressed(k)

    local p = lightColors[1]

    if k == "up" then
        p.y = math.max(p.y - 1, 1)
    elseif k == "down" then
        p.y = math.min(p.y + 1, mapHeight)
    elseif k == "left" then
        p.x = math.max(p.x - 1, 1)
    elseif k == "right" then
        p.x = math.min(p.x + 1, mapWidth)
    end

end

function love.draw()
    drawMap()
end
My avatar code for the curious :D V1, V2, V3.
User avatar
Gunroar:Cannon()
Party member
Posts: 1091
Joined: Thu Dec 10, 2020 1:57 am

Re: How to blend light colours?

Post by Gunroar:Cannon() »

Nice! Thanks, adding seems better. But now I rememebr the problem with it. If there are too many colours I just get white. Like 3 oranges (1,.7,.7) will give me values higher than 1 and I'll get white. Any fixes?

(Those debugging numbers on each tile are r,g,b values to 2 significant figures I think.)
Okay-ish
Okay-ish
Screenshot_2023-04-19-10-15-45.png (186.33 KiB) Viewed 2552 times
White (should be orange)
White (should be orange)
Screenshot_2023-04-19-10-16-24.png (284.52 KiB) Viewed 2552 times
The risk I took was calculated,
but man, am I bad at math.

-How to be saved and born again :huh:
User avatar
Bigfoot71
Party member
Posts: 287
Joined: Fri Mar 11, 2022 11:07 am

Re: How to blend light colours?

Post by Bigfoot71 »

Well, it's normal behavior that when there's too much light the results become white if the sum of all components exceeds 1, I don't know what to say. It's like in real life, white is just the mixture of all spectra.

You could normalize the values but the result won't be more realistic, even though artistically it might work, it's up to you to decide.
I think the best way to do it in this case (if it's good at all) would be to do it like this in my opinion (but it should generally darken the result):

Code: Select all

-- Find the maximum value
local max = 0
for i = 1, #color do
  if color[i] > max then
    max = color[i]
  end
end

-- Normalization of values
for i = 1, #color do
  color[i] = color[i] / max  -- ( `x * (1/max)` can be faster if the division is calculated beforehand )
end
Or added a max value (one possible way):

Code: Select all

local function addc(c1, c2, max)
    for i = 1, #c2 do
        c1[i] = math.min(c1[i]+c2[i], max[i])
    end
end
Maybe there are other better solutions but I couldn't tell, or it doesn't immediately come to mind.

Otherwise if there is really too much white, reduce the light sources or change their values, this is perhaps the most "pragmatic" answer :ultraglee:
My avatar code for the curious :D V1, V2, V3.
User avatar
Bigfoot71
Party member
Posts: 287
Joined: Fri Mar 11, 2022 11:07 am

Re: How to blend light colours?

Post by Bigfoot71 »

Bigfoot71 wrote: Wed Apr 19, 2023 10:33 am Well, it's normal behavior that when there's too much light the results become white if the sum of all components exceeds 1, I don't know what to say. It's like in real life, white is just the mixture of all spectra.

You could normalize the values but the result won't be more realistic, even though artistically it might work, it's up to you to decide.
I think the best way to do it in this case (if it's good at all) would be to do it like this in my opinion (but it should generally darken the result):

Code: Select all

-- Find the maximum value
local max = 0
for i = 1, #color do
  if color[i] > max then
    max = color[i]
  end
end

-- Normalization of values
for i = 1, #color do
  color[i] = color[i] / max  -- ( `x * (1/max)` can be faster if the division is calculated beforehand )
end
Or added a max value (one possible way):

Code: Select all

local function addc(c1, c2, max)
    for i = 1, #c2 do
        c1[i] = math.min(c1[i]+c2[i], max[i])
    end
end
Maybe there are other better solutions but I couldn't tell, or it doesn't immediately come to mind.

Otherwise if there is really too much white, reduce the light sources or change their values, this is perhaps the most "pragmatic" answer :ultraglee:
Edit: After some tests, maybe this result would suit you better, I took as an example only colors worth (1,.7,.7):

With addc:
Image

With a custom mixc function (sorry for the quality but there is indeed a difference in hue, the white comes less quickly):
Image

The function in question:

Code: Select all

local function mixc(c1, c2)
    c1[1] = c1[1] + (1 - c1[1]) * c2[1]
    c1[2] = c1[2] + (1 - c1[2]) * c2[2]
    c1[3] = c1[3] + (1 - c1[3]) * c2[3]
end
Otherwise, another way to approach the problem would be to use subtractive blends to start from the base color or white and go to black, rather than from black to go to light. And indeed one of the best way would be to do as you said in your first post, but we will come across the same problems that you mentioned in this case, because 1*1=1 and 0.7*0.7=0.48999999999999994 so ​​the strongest component will always dominate after multiplication:
Image

Code: Select all

local function mixc(c1, c2)
    for i = 1, #c1 do
        c1[i] = c1[i] * c2[i]
    end
end
In short, try different blending until you find the one that suits you personally, but in terms of realism I think the addition would be the best in this case, maybe I'm wrong.

Otherwise you forget the idea of ​​mixing the lights and you only darken the colors according to the distance.

(Small mistake, I quoted instead of edited without realizing it, sorry)
My avatar code for the curious :D V1, V2, V3.
User avatar
Gunroar:Cannon()
Party member
Posts: 1091
Joined: Thu Dec 10, 2020 1:57 am

Re: How to blend light colours?

Post by Gunroar:Cannon() »

Wow, thanks. Yeah, you're right. A lot of light would appear white ( though wouldn't a lot of green light still appear green?).

One of these should hopeully work.
The risk I took was calculated,
but man, am I bad at math.

-How to be saved and born again :huh:
User avatar
Bigfoot71
Party member
Posts: 287
Joined: Fri Mar 11, 2022 11:07 am

Re: How to blend light colours?

Post by Bigfoot71 »

Gunroar:Cannon() wrote: Wed Apr 19, 2023 3:24 pm A lot of light would appear white ( though wouldn't a lot of green light still appear green?).
With the additive method the sum of only two green lights will always appear green, for example:

Code: Select all

(0,1,0) + (0,1,0) = (0,2,0) -- Will always give the same green
(1,0,0) + (0,1,0) = (1,1,0) -- Red color plus green color will give yellow color
(1,1,0) + (0,0,1) = (1,1,1) -- Yellow color plus blue color will result in white color
But I tried to hack something with a linear function like this (where `t` is the intensity obtained via the distance from the light source):

Code: Select all

local function lerpColor(c1, c2, t)
    c1[1] = c1[1] * (1 - t) + c2[1] * t
    c1[2] = c1[2] * (1 - t) + c2[2] * t
    c1[3] = c1[3] * (1 - t) + c2[3] * t
end
And here is what it gives:
Image

However it should be noted that the color mixing behaves in a way that the last color of the table will have a greater weight than the others, besides this fact, the rendering may be a little more "realistic" in a way because what I mentioned before is an approximation. To be more precise, in real life light blends don't always produce white. There are many factors at play, such as the colors of the surfaces involved, their reflectivity and texture, etc. And in some of these cases one color could subtract another. The calculations necessary to obtain a 100% realistic result will be much more complex, and at this level the use of a shader would be better. I think that here, whatever solution you choose, you will have a good compromise.

But if you want to go even further, according to my research, we could consider a method of subtraction using CMY colors that we convert back to RGB, or also by doing the work from HSL colors and converting back to RGB, but it gets more computationally advanced and that's where a shader would be more appropriate.

But be sure that if I find a better solution not too cumbersome to implement to your problem I will share it because I am also interested.

However without going too far, the addition of color which gives white, for the management of 2D light is quite suitable, we are not in high-flying simulation and the end user should not ask too many questions.

You can see how other Löve libraries handle light:
https://github.com/dylhunn/simple-love-lights
https://github.com/xiejiangzhi/light
https://github.com/speakk/lighter

The full last try code:

Code: Select all

-- Initialize light colors --

local lightColors = {

    { -- yellow (player)
        x = 1, y=1,
        1, 1, 0
    };

    { -- red
        x = 5, y=5,
        1, 0, 0
    },

    { -- green
        x = 15, y=10,
        0, 1, 0
    },

    { -- blue
        x = 10, y=15,
        0, 0, 1
    },

}

local mapWidth = 100
local mapHeight = 75
local tileWidth = 8
local tileHeight = 8

-- Main functions --

local function lerpColor(c1, c2, t)
    c1[1] = c1[1] * (1 - t) + c2[1] * t
    c1[2] = c1[2] * (1 - t) + c2[2] * t
    c1[3] = c1[3] * (1 - t) + c2[3] * t
end

local function drawMap()

    for x = 1, mapWidth do
        for y = 1, mapHeight do

            local color = {0, 0, 0}

            for i, lc in ipairs(lightColors) do
                local distance = math.sqrt((x-lc.x)^2+(y-lc.y)^2)
                local intensity = 1 / (distance + 1)
                lerpColor(color, lc, intensity)
            end

            love.graphics.setColor(color)
            love.graphics.rectangle("fill", (x-1)*tileWidth, (y-1)*tileHeight, tileWidth, tileHeight)

        end
    end
end

function love.keypressed(k)

    local p = lightColors[1]

    if k == "up" then
        p.y = math.max(p.y - 1, 1)
    elseif k == "down" then
        p.y = math.min(p.y + 1, mapHeight)
    elseif k == "left" then
        p.x = math.max(p.x - 1, 1)
    elseif k == "right" then
        p.x = math.min(p.x + 1, mapWidth)
    end

end

local scale = 1

function love.wheelmoved(x, y)
    scale = scale + y
end

function love.draw()
    love.graphics.push()
    love.graphics.scale(scale)
    drawMap()
    love.graphics.pop()
end
My avatar code for the curious :D V1, V2, V3.
Post Reply

Who is online

Users browsing this forum: Google [Bot], Kakkassery_Joseph, Majestic-12 [Bot] and 2 guests