sRGB and Alpha Blending? I'm confused

Started by
5 comments, last by silikone 3 years ago

Hello,

I am so confused about sRGB alpha blending

if you have a destination of pure white (255,255,255) aka (1,1,1)

and a source of pure black (0,0,0)

then we know these values are the same in sRGB and Linear (1^2.2=1) and (0^2.2=0).

now, we want to blend the black with 0.5 alpha.

(src * alpha) + (dest * (1-alpha)) = new color

((0,0,0) * 0.5) + ((1,1,1) * (1-0.5)) = (0.5,0.5,0.5)

now please, what color space is this new color?? If we say its linear, then later on display we have to convert it to srgb and so we do (0.5,0.5,0.5)^(1.0/2.2) = WRONG (too bright).



if the new color is sRGB already, isnt it funny we can do from linear colors to sRGB inside the alpha blending suddenly?

Advertisement

You are using a contrive example. Blending is a linear operation which means the inputs are linear and by definition the output. If your inputs are any but( non-linear) then you decide what the output should be.

actually: (0.5,0.5,0.5)^(1.0/2.2) = RIGHT! = real half brightness between black and white.

So as I understand it, all blending should be be done in linear space. sRGB is not linear in regards to actual light intensity, it is meant to be just for getting better use out of the limited 8bit precision and because of some details of how CRT displays worked. This means that 128 / 0.5 is not half way between white and black in sRGB, rather 188 / 0.737 is (the 0.5^(1/2.2) you gave is 180 or 0.707). So yes, this is “brighter”, but that is by design, and when you blend lighting, etc. should look correct.

sRGB is also not a 2.2 gamma curve. It is close, but not actually a single value. For each RGB channel, srgb = linear < 0.031308 ? 12.92 * linear : 1.055 * linear^(1/2.4) - 0.055 for 0-1. So to take that 0.5 value, it is the 2nd case so `1.055 * 0.5^(1/2.4) - 0.055 = 0.73536`(if you convert that to 8bit it would want to be 187.516 which obviously it can't, rounding to the 188 mentioned). The alpha channel is linear.

Also to avoid precision loss, you want more than 8 bits for any linear colour value (consider if you mapped 8bit 0-255 to 0-255 some inputs give duplicate outputs and some outputs are impossible), since shaders often use floats this isn't an issue, unless you had some intermediate 8bit texture that used a different colour space.

Direct3D and OpenGL can do this automatically for you. When they sample a texture they can give you the linear rgb float, and when they write the shader output to the render target they convert back to sRGB. So basically your shaders don't care about sRGB and get to use linear intensity, which works for blending, lighting, etc. Likewise they also handle the conversion for the alpha output blending. e.g. in D3D11 and OGL.

// on my system at least, D3D11CreateDeviceAndSwapChain gives me this automatically if inspect swap_chain->GetBuffer(), buffer->GetDesc()
// also correct for most textures, since often they are saved in sRGB space (PNG etc. have some metadata that can tell you this as well)
D3D11_TEXTURE2D_DESC desc;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;

// Windows OpenGL
piAttribIList[...] = WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB
piAttribIList[...] = GL_TRUE
wglChoosePixelFormatARB(hdc, piAttribIList, pfAttribFList, nMaxFormats, piFormats, nNumFormats);
glEnable(GL_FRAMEBUFFER_SRGB); // Tell OpenGL if the destination is sRGB (e.g. the framebuffer), that the fragment output is linear and to convert it
// also tell it if a texture is sRGB
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);  
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB_ALPHA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);  

I believe D3D9 had something similar.

So this is kinda weird. “Linear” in this context means “mathematically linear”, in that there's no funky curve applied to the color values (properly called a “electro-optical transfer function”, but you may also hear it called a “gamma” curve). It does not mean that you'll perceive the resulting brightness as being “linear”! sRGB with its EOTF applied (which a lot of people just call “sRGB”) actually results something closer to “perceptually linear” since it more closely matches the logarithmic response of our eyes to increases in brightness. So in other words 0.5 in sRGB w/ EOTF will look about twice as bright as 0.25 with the same curve applied, but that won't be the case for 0.5 in “linear” sRGB compared to 0.25. It's basically the same reason why a gradient in “linear” space looks funky and squished compared to a gradient in non-linear sRGB.

There are two main reasons for working in a linear color space:

  • When doing lighting calculations it makes more sense to do things without a curve applied, since that will more closely match the physical behavior of real lighting
  • Doing downsampling and other filtering operations won't result in the correct intensity if done in non-linear space
  • Doing blending and other operations on non-linear sRGB can take some weird paths through the color space. There's an example in this blog post:

SyncViews said:

Also to avoid precision loss, you want more than 8 bits for any linear colour value (consider if you mapped 8bit 0-255 to 0-255 some inputs give duplicate outputs and some outputs are impossible), since shaders often use floats this isn't an issue, unless you had some intermediate 8bit texture that used a different colour space.

This is too often ignored. I'd actually wager that a gamma of 2 is perceptually a better approximation than 2.2. Even though the curve very closely matches the latter, there is a huge difference at the very dark levels that you can't make out on a graph. 2.2 at its darkest level is SIXTY times brighter than that of sRGB. Not even comparable.

This topic is closed to new replies.

Advertisement