0

So I've got some code that's intended to generate a Linear Gradient between two input colors:

struct color {
    float r, g, b, a;
}

color produce_gradient(const color & c1, const color & c2, float ratio) {
    color output_color;
    output_color.r = c1.r + (c2.r - c1.r) * ratio;
    output_color.g = c1.g + (c2.g - c1.g) * ratio;
    output_color.b = c1.b + (c2.b - c1.b) * ratio;
    output_color.a = c1.a + (c2.a - c1.a) * ratio;
    return output_color;
}

I've also written (semantically identical) code into my shaders as well.

The problem is that using this kind of code produces "dark bands" in the middle where the colors meet, due to the quirks of how brightness translates between a computer screen and the raw data used to represent those pixels.

So the questions I have are:

  • Do I need to correct for gamma in the host function, the device function, both, or neither?
  • What's the best way to correct the function to properly handle gamma? Does the code I'm providing below convert the colors in a way that is appropriate?

Code:

color produce_gradient(const color & c1, const color & c2, float ratio) {
    color output_color;
    output_color.r = pow(pow(c1.r,2.2) + (pow(c2.r,2.2) - pow(c1.r,2.2)) * ratio, 1/2.2);
    output_color.g = pow(pow(c1.g,2.2) + (pow(c2.g,2.2) - pow(c1.g,2.2)) * ratio, 1/2.2);
    output_color.b = pow(pow(c1.b,2.2) + (pow(c2.b,2.2) - pow(c1.b,2.2)) * ratio, 1/2.2);
    output_color.a = pow(pow(c1.a,2.2) + (pow(c2.a,2.2) - pow(c1.a,2.2)) * ratio, 1/2.2);
    return output_color;
}

EDIT: For reference, here's a post that is related to this issue, for the purposes of explaining what the "bug" looks like in practice: https://graphicdesign.stackexchange.com/questions/64890/in-gimp-how-do-i-get-the-smudge-blur-tools-to-work-properly

Community
  • 1
  • 1
Xirema
  • 19,889
  • 4
  • 32
  • 68

4 Answers4

0

I think there is a flaw in your code. first i would make sure that 0 <= ratio <=1

second i would use the formula c1.x * (1-ratio) + c2.x *ratio

the way you have set up your calculations at the moment allow for negative results, which would explain the dark spots.

Adl A
  • 183
  • 1
  • 8
  • The formula you provided is mathematically identical to mine. Except your formula forgets the necessary parenthesis around `1-ratio`. – Xirema Feb 29 '16 at 19:31
  • however, if either **c1.x** or **c2.x** is positive then a valid **ratio** and by that i mean `0 <= ratio <=1` would not produce **output_color.x <= 0** it could very well be that you are encountering rounding errors and you need to use `unsigned float` – Adl A Feb 29 '16 at 19:38
  • I'm not concerned with bounding the input domain of `ratio`. The problem occurs with `ratio` values between 0.25 and 0.75. – Xirema Feb 29 '16 at 19:43
  • the dark bands problem is very weird then. I can't see your whole code, but under the assumption that the calculation return expected values, what i would recommend is converting your color from rgb space into hsl space for the calculation. it is a bit of an overkill, but as i said, without knowing the rest of the code... – Adl A Feb 29 '16 at 19:51
  • I'm adding this link to my post, as it seems to be the most obviously related post I can find to this issue: http://graphicdesign.stackexchange.com/questions/64890/in-gimp-how-do-i-get-the-smudge-blur-tools-to-work-properly – Xirema Feb 29 '16 at 20:25
0

There is no pat answer for when you have to worry about gamma.

You generally want to work in linear color space when mixing, blending, computing lighting, etc.

If your inputs are not in linear space (e.g., that are gamma corrected or are in some color space like sRGB), then you generally want to convert them at once to linear. You haven't told us whether your inputs are in linear RGB.

When you're done, you want to ensure your linear values are corrected for the color space of the output device, whether that's a simple gamma or other color space transform. Again, there's no pat answer here, because you have to know if that conversion is being done for you implicitly at a lower level in the stack or if it's your responsibility.

That said, a lot of code gets away with cheating. They'll take their inputs in sRGB and apply alpha blending or fades as though they're in linear RGB and then output the results as is (probably with clamping). Sometimes that's a reasonable trade off.

Adrian McCarthy
  • 45,555
  • 16
  • 123
  • 175
0

your problem lies entirely in the field of perceptual color implementation. to take care of perceptual lightness aberrations you can use one of the many algorithms found online one such algorithm is Luma

float luma(color c){
return 0.30 * c.r + 0.59 * c.g + 0.11 * c.b;
}

at this point I would like to point out that the standard method would be to apply all algorithms in the perceptual color space, then convert to rgb color space for display.

colorRGB --(convert)--> colorPerceptual --(input)--> f (colorPerceptual) --(output)--> colorPerceptual' --(convert)--> colorRGB

but if you want to adjust for lightness only (perceptual chromatic aberrations will not be fixed), you can do it efficiently in the following manner

//define color of unit lightness. based on Luma algorithm 
color unit_l(1/0.3/3, 1/0.59/3, 1/0.11/3);

color produce_gradient(const color & c1, const color & c2, float ratio) {
    color output_color;
    output_color.r = c1.r + (c2.r - c1.r) * ratio;
    output_color.g = c1.g + (c2.g - c1.g) * ratio;
    output_color.b = c1.b + (c2.b - c1.b) * ratio;
    output_color.a = c1.a + (c2.a - c1.a) * ratio;

    float target_lightness = luma(c1) + (luma(c2) - luma(c1)) * ratio; //linearly interpolate perceptual lightness
    float delta_lightness = target_lightness - luma(output_color); //calculate required lightness change magnitude

    //adjust lightness
    output_color.g += unit_l.r * delta_lightness;
    output_color.b += unit_l.g * delta_lightness;
    output_color.a += unit_l.b * delta_lightness;

    //at this point luma(output_color) approximately equals target_lightness which takes care of the perceptual lightness aberrations

    return output_color;
}
Adl A
  • 183
  • 1
  • 8
0

Your second code example is perfectly correct, except that the alpha channel is generally not gamma corrected so you shouldn't use pow on it. For efficiency's sake it would be better to do the gamma correction once for each channel, instead of doubling up.

The general rule is that you must do gamma in both directions whenever you're adding or subtracting values. If you're only multiplying or dividing, it makes no difference: pow(pow(x, 2.2) * pow(y, 2.2), 1/2.2) is mathematically equivalent to x * y.

Sometimes you might find that you get better results by working in uncorrected space. For example if you're resizing an image, you should do gamma correction if you're downsizing but not if you're upsizing. I forget where I read this, but I verified it myself - the artifacts from upsizing were much less objectionable if you used gamma corrected pixel values vs. linear ones.

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622