# Why does using additive blending with a linear colour dropoff result in dark fringes?

I'm developing a tool which allows for placing of lights in a black space, but when 2 lights are close to one another the areas near to where they overlap appear to darken:

I was able to reproduce this using Krita by using a brush which drops off linearly with distance (a modified default "Airbrush Soft"):

The brush size is 25px and the two colours being used in this example are #d78a1f and #bb962b.

When using a non-linear softness the effect isn't present however the orange (on the right) appears to "bleed" through and be more visible than the yellow (on the left hand side of where the two colours meet):

Is there a name for this apparent darkening, what is the cause and how can I mitigate it?

EDIT I have got 2 answers based on my Krita example which have helped me resolve the issue, but don't explain the exact cause. The Krita example is only a way to create something which resembles my problem shown in my 1st image. In my application there's no transparency.

The RGB values of the image pixels are calculated in my program pixel by pixel based on distance from lamps. The resulting colour of a pixel is found by multiplying the red, green and blue components of the lamp colour by distance dependent brightness (`rgb(lamp.red * brightness, lamp.green * brightness, lamp.blue * brightness)`). The brightness falls from 1 to 0 as the distance from the lamp grows from 0 to the light spot radius of the lamp.

This is done for each image pixel for each lamp and then all of the values are added together in my program.

See this image (created again using Krita for demonstration purposes):

Layer 1 and layer 2 are both opaque. Their color is solid black, each with a circle drawn on them. When the RGB values of the layers are added pixel by pixel we still see those dark edges.

• You are stepping into a rabbit hole that is way bigger than you initially thought. Rocket science is easier than this. Commented Oct 4, 2020 at 6:01

It looks like this has something to do with using the "Soft" mask option in Krita. I tried to replicate this in other applications such as GIMP and Photoshop, but failed. So, it seems to be a peculiarity of Krita's brush engine. As for the reason, perhaps reach out to the Krita developers. I suppose it will depend on the maths that lies behind the implementation of Krita's brush engine.

There is a way to mitigate it though. Instead of using the "Soft" mask, choose "Gaussian".

Example showing comparison of Soft versus Guassian

• Yes, this did end up being the problem, in the end in the tool I'm developing I didn't implement a Gaussian mask and instead changed my equation for brightness (with d being normalised distance from the lamp), from `b = 1 - d` to `b = 0.7499 * d * d - 1.7436 * d + 0.9964`. This lets me fine tune it to my needs and allows me to avoid the "bright spot" that's in the middle of the gaussian mask. I'm not sure it's an issue with Krita (I did after all, implement the exact same problem myself), but rather just the nature of using a linear mask. Commented Oct 4, 2020 at 10:11
• Here's a before and after for reference: i.sstatic.net/LabKG.png Commented Oct 4, 2020 at 10:16
• @Nick Color calculations are quite complicated subject. Its not even clear how the values should be handled, should the engine handle the non linearity of color silently or should it not. This is very implementation specific... I think you should work color correct but then none of the assumptions wouldn't be correct Commented Oct 4, 2020 at 10:16

Dropping linearly = increase transparency linearly. You expect that at some point, say at 50% point of the slope the brightness should be full 100 % if there's in the same place also a 50% point of the lower light layer.

But it isn't. If the lower layer is also 50% transparent there can be seen some black through. The total brightness is only 75%.

See this image which has black in the bottom and 2 full white layers:

The top layer was made by duplicating Layer 2. The layer masks of the top layer was inverted after it. In the middle there's a darker zone because brightness isn't 100%, no matter the sum of white layer opacities is everywhere 100%.

Blending modes are = normal. Changing them to Add cause nothing as you can calculate if you want.

Your lights should be opaque gradients from bright color to black. Then they would work with blending mode Add as you expect. See this example:

The top and mid layers have both blending mode= Add. Both layers are opaque. The top layer = the mid layer inverted.

ADD2: Your edited demo starts to work properly in Photoshop if you change to 32 bit/channel color mode. See the next screenshot:

GIMP with default settings works also like expected, no dark zone problem occurs.

A little resembling case with Photoshop gave an idea that "gamma" has some role also in this case. Experiments with Krita brought the light. The problem really is caused by the gamma property of the used RGB color space. Color mixing with the usual non-linear gamma generates darker results than one expects. Linear gamma (=1.0) hasn't that problem.

In Krita you have plenty of options to select from. You can select a version of sRGB with linear gamma. Krita recommends to use also 16bit color depth for acceptably dense set of available colors; 8bit integer/channel with linear gamma can cause banding in well visible colors.

In the next image a copy is made of your layer 2 and it's placed on the top of the layer 1, blending is Addition. The image is converted to linear gamma RGB color space with 16 bit/channel depth. The problem vanished.

In your program you should calculate the color mixing in linear gamma colorspace. I am not a competent mathematician, so I cannot give any ready to use equations. Sorry. I guess you should convert the compensated RGB numbers before the weighted addition with high enough calculation accuracy to avoid generating artifacts.

• The colours that I'm dealing with in the tool I'm writing are RGB only, 50% across a gradient from #FFFFFF to #000000 would be #808080, adding these would be #FFFFFF (after accounting for clamping to the range, as, of course, white is as bright as it gets). It's not clear to me from this where transparency comes into it. Commented Oct 4, 2020 at 10:06
• So, you think there's no transparency in your brush strokes? Right? Check it by painting with brush blending mode = Normal into a blank layer which has blending mode = Normal. Have a white background layer. If your smooth brush really makes a gradient from bright color to black your painted stroke should have black edge. If there's a black edge my answer is a piece of guessed crap which should be deleted immediately.
– user82991
Commented Oct 4, 2020 at 10:24
• You're right in that that is how it functions in Krita, Krita was only intended as a demonstration of how it can be reproduced, although that does appear to have come back to haunt me. The first screenshot in my post is from my tool which has no transparency support yet produces the same effect. Commented Oct 4, 2020 at 10:41
• How the color of the edge pixels is calculated in your system? Is there some internal anti-aliasing system in use? Or is everything which is painted 100% opaque pixels?
– user82991
Commented Oct 4, 2020 at 10:48
• 100% opaque pixels, a brightess for a pixel is calculated based on distance from lamp (ranging from 0 to 1), and then the resulting colour of a pixel is found by multiplying the red green and blue components of the lamp colour by that brightness (`rgb(lamp.r * brightness,lamp.g * brightness,lamp.b * brightness)`). This is done for each pixel for each lamp and then all of the values are added together. Commented Oct 4, 2020 at 10:51

You are making the naive assumption that color is linear. Its not there is a complicated transform on top of your color values. What does it mean that color is not linear. It means that 0.25 + 0.25 is not unexpectedly 0.5 likewise 0.5+0.5 is not 1.0.

This is kindof suprising but true. Its not really shameful to make this assumption even top tier early graphics and rendering programners took several years to figure out that the reason their renders looked wrong was that they had neglected to account for this. It took software vendors even longer, several decades in fact. So by the time we generally realized there was a huge problem in how we had done graphics many, many people out there that relied on "wrong" ways to do it. Heck, adobe still renders a buch of antialiasing scientific objectively wrong but we are so used to it nobody notices.

As a result it is somewhat unsure how things should be done.

• Since people ofter make the linearity assumption, should we try to calculate in linear space and then convert to nonlinear.

• If we do do we present the linear result or the real result.

• Or do we just let the user do what they were always doing blissfully ignorantly.

In fact, we do a combination of all of these things, there are lots of upsides of each method. The entire situation is so messed up that you cant even say that one way over the other is more correct. And correct from what viewpoint. So if you want to know why krita does what it does read the source.

So this leads to the question is linear dropof making the assumption pixels intensities are linear or not? And if should it or not. In a drawing application it probably does not matter. But in your simulation you should account for this unless you want to have the same problems as 1990-2010 3D graphics.