Reversing gamma correction to get accurate linear color values

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
grump
Party member
Posts: 947
Joined: Sat Jul 22, 2017 7:43 pm

Reversing gamma correction to get accurate linear color values

Post by grump »

I want to use the color information in a Canvas of format 'r8' to select layers from an ArrayImage in a shader. This is working fine, unless gamma correction is enabled.

Shader code:

Code: Select all

uniform ArrayImage arrayCanvas;

vec4 effect(vec4 color, Image tex, vec2 uv, vec2 fc) {
	float layer = floor(Texel(tex, uv).r * 255.0 + .5);
	return Texel(arrayCanvas, vec3(uv * 16.0, layer));
}
Without gamma correction:
Image

The image shows all 256 layers of an ArrayImage, and each layer is showing its index. The red background is the Canvas from which the layer index is read.
The layers are shown as expected, the layer indices have no discontinuities or rounding errors.

The code that generated the layer selection canvas:

Code: Select all

local layerSelect = lg.newCanvas(512, 512, { format = 'r8' })
lg.setCanvas(layerSelect)
for i = 0, 255 do
	local col = i / 255
	lg.setColor(col, col, col, 1.0)
	local x, y = i % 16, math.floor(i / 16)
	lg.rectangle('fill', x * 32, y * 32, 32, 32)
end
With gamma correction enabled, you have to account for the non-linear change in the color values. So I changed the color calculation, using love.math.linearToGamma:

Code: Select all

for i = 0, 255 do
	local col = love.math.linearToGamma(i / 255) -- this line is the relevant change
	lg.setColor(col, col, col, 1.0)
	local x, y = i % 16, math.floor(i / 16)
	lg.rectangle('fill', x * 32, y * 32, 32, 32)
end
This does not give correct results
Image

For example: layers 2 and 4 appear twice instead of 3 and 5. More severe errors with higher numbers. I tried rounding, floor, ceil, in the shader to no avail. I also tried unGammaCorrectColor in the shader, which gave me even weirder distribution of values in any case.

Disabling gamma correction while the index canvas is generated is not possible; it is enabled in conf.lua and can't be changed at runtime. A way to selectively disable gamma correction for some Canvases does not seem to exist. The index canvas is highly dynamic and can't be precomputed in my use case.

Any ideas how to solve this problem?
Attachments
linear.love
(778 Bytes) Downloaded 141 times
grump
Party member
Posts: 947
Joined: Sat Jul 22, 2017 7:43 pm

Re: Reversing gamma correction to get accurate linear color values

Post by grump »

The attachment with the gamma correct love file disappeared after the post was created. Here it is.
Attachments
linearToGamma.love
(790 Bytes) Downloaded 148 times
User avatar
pgimeno
Party member
Posts: 3548
Joined: Sun Oct 18, 2015 2:58 pm

Re: Reversing gamma correction to get accurate linear color values

Post by pgimeno »

Not sure, but it seems that Löve by default uses the imprecise formulas.

https://github.com/love2d/love/blob/f51 ... #L173-L174

How about disabling gamma and applying the gamma yourself in the shader? Is that viable?
User avatar
slime
Solid Snayke
Posts: 3132
Joined: Mon Aug 23, 2010 6:45 am
Location: Nova Scotia, Canada
Contact:

Re: Reversing gamma correction to get accurate linear color values

Post by slime »

grump wrote: Wed Oct 28, 2020 4:14 pm A way to selectively disable gamma correction for some Canvases does not seem to exist.
It does - see the PixelFormat wiki page. The default pixel format for Canvases if you don't give your own value is "normal", which selects between rgba8 and srgba8 depending on whether gamma correction is active. If you want a canvas whose pixel contents are encoded as linear RGB, you can do newCanvas(width, height, {format="rgba8"}).

Similarly, you can create a linear image with newImage(..., {linear=true}).

That being said, all pixel formats aside from the default / srgba8 are linear (linear meaning there is no conversion between sRGB and linear RGB when sampling from or rendering to the texture). r8 is linear. What isn't linear is the color value interpreted by love.graphics.setColor.

You should use a shader that has a custom uniform for the index value you're drawing the rectangles with, instead of treating it as a color with setColor. Any linear RGB <-> sRGB conversion involves floating point arithmetic, which you don't need. And setColor in particular may store the pre-linearized value in 8 bits.

Here's the modified main.lua which does that:

Code: Select all

local lg = love.graphics

local arrayCanvas = lg.newCanvas(32, 32, 256)
arrayCanvas:setWrap('repeat')

lg.push("all")
for i = 1, 256 do
	lg.setCanvas(arrayCanvas, i)
	lg.print(i)
end
lg.pop()

local rectangleShader = lg.newShader[[
uniform int index;

vec4 effect(vec4 color, Image tex, vec2 uv, vec2 fc) {
	float normindex = (index + 0.5) / 255.0;
	return vec4(normindex, normindex, normindex, 1.0);
}
]]

local layerSelectCanvas = lg.newCanvas(512, 512, { format = 'r8' })
lg.push("all")
lg.setCanvas(layerSelectCanvas)
lg.setShader(rectangleShader)
for i = 0, 255 do
	rectangleShader:send("index", i)
	local x, y = i % 16, math.floor(i / 16)
	lg.rectangle('fill', x * 32, y * 32, 32, 32)
end
lg.pop()

local layerSelectShader = lg.newShader([[
	uniform ArrayImage arrayCanvas;

	vec4 effect(vec4 color, Image tex, vec2 uv, vec2 fc) {
		float layer = floor(Texel(tex, uv).r * 255.0 + .5);
		return Texel(arrayCanvas, vec3(uv * 16.0, layer));
	}
]])

layerSelectShader:send('arrayCanvas', arrayCanvas)

function love.draw()
	lg.draw(layerSelectCanvas)

	lg.setShader(layerSelectShader)
	lg.draw(layerSelectCanvas)
	lg.setShader()
end

grump
Party member
Posts: 947
Joined: Sat Jul 22, 2017 7:43 pm

Re: Reversing gamma correction to get accurate linear color values

Post by grump »

Duh... yes, my bad. I did it wrong in the test code, but I'm already using a uniform in the actual code.

Turns out the real reason why the wrong layers are selected is this bug. In the code where this happens, more processing is involved with multiple intermediate results and readbacks, and somewhere along the line it triggers the bug and messes up the texture.

Here is test code that more closely resembles the processing that happens in the actual code:

Code: Select all

local lg = love.graphics

local arrayCanvas = lg.newCanvas(32, 32, 256)
arrayCanvas:setWrap('repeat')
for i = 1, 256 do
	lg.setCanvas(arrayCanvas, i)
	lg.print(i)
end

local rectShader = lg.newShader([[
	uniform float layer;

	vec4 effect(vec4 color, Image tex, vec2 uv, vec2 fc) {
		return vec4(layer, layer, layer, 1.0);
	}
]])

local layerSelectCanvas = lg.newCanvas(512, 512, { format = 'r8' })
lg.setCanvas(layerSelectCanvas)
lg.setShader(rectShader)
for i = 0, 255 do
	rectShader:send('layer', i / 255)
	local x, y = i % 16, math.floor(i / 16)
	lg.rectangle('fill', x * 32, y * 32, 32, 32)
end
lg.setCanvas()
lg.setShader()

local intermediateImage = lg.newImage(layerSelectCanvas:newImageData())

 -- works correctly with `r8`, fails with `rgba8`
local intermediateCanvas = lg.newCanvas(512, 512, { format = 'rgba8' })
intermediateCanvas:renderTo(function() lg.draw(intermediateImage) end)
local finalImage = lg.newImage(intermediateCanvas:newImageData())

local layerSelectShader = lg.newShader([[
	uniform ArrayImage arrayCanvas;

	vec4 effect(vec4 color, Image tex, vec2 uv, vec2 fc) {
		float layer = floor(Texel(tex, uv).r * 255.0 + .5);
		return Texel(arrayCanvas, vec3(uv * 16.0, layer));
	}
]])

layerSelectShader:send('arrayCanvas', arrayCanvas)

function love.draw()
	lg.draw(finalImage)

	lg.setShader(layerSelectShader)
	lg.draw(finalImage)
	lg.setShader()
end
Image

This is pretty much the same problem as issue #1556. It works correctly when using `r8` instead of `rgba8` though.
User avatar
slime
Solid Snayke
Posts: 3132
Joined: Mon Aug 23, 2010 6:45 am
Location: Nova Scotia, Canada
Contact:

Re: Reversing gamma correction to get accurate linear color values

Post by slime »

You can use something like this until love's sRGB support in imagedata's format APIs gets some changes:

Code: Select all

function newImageFromCanvas(canvas)
	local linear = canvas:getFormat()~= "srgba8" and lg.isGammaCorrect()
	return lg.newImage(canvas:newImageData(), {linear=linear})
end
It's not really a code bug in love, more a mismatch between how different APIs handle sRGB pixel formats (so an API design flaw I suppose).
Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot] and 164 guests