Welcome, Guest.
Username: Password: Remember me

TOPIC: How to find average luma within a frame

How to find average luma within a frame 1 year 5 months ago #1

so i've been experimenting with a shader that can load one LUT for daytime, and another for night. it works, but my luma detection algorithm is extremely hack-y.

it basically looks like this:
float CalcLumaMiddleScreen() {
	float lumaAvg = 0.0;

	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(0, 0)).a;
	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(1, 0)).a;
	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(0, 1)).a;
	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(1, 1)).a;

	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(-1, -1)).a;
	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(2, 0)).a;
	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(0, 2)).a;
	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(2, 2)).a;

	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(-4, -4)).a;
	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(5, 0)).a;
	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(0, 5)).a;
	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(5, 5)).a;

	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(-10, -10)).a;
	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(11, 0)).a;
	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(0, 11)).a;
	lumaAvg += tex2Doffset(SLogSampler, (float2(BUFFER_WIDTH/2, BUFFER_HEIGHT/2)), int2(11, 11)).a;

	return lumaAvg/16;
}

it basically takes a bunch of points near-ish to the center of the screen, add up their luma, and then dividing it by 16 because there are 16 points used.

this "works", but i have gotten instances where the game would rapidly switch between the two different LUTs, because the averaged luma value returned from above just happens to be close to the cutoff point.

so my question is: is there a way for me to take the average luma value of the entire frame, as opposed to just 16 random-ish points?
The administrator has disabled public write access.

How to find average luma within a frame 1 year 5 months ago #2

Repeatedly blurring and down scaling is what you are looking for. Calculating the average of an image is sadly not so easy. What you can do (but what might compile slowly) is making a pass that renders into a 1x1 render target (so it only runs once, not millions of times like fullscreen shaders) and make a chain of loops that sums up every pixel onscreen and calculates the average of that. Actually, the more pixel you read, the better but 200x200 samples are enough I think. This may sound crazy but as you only write to 1 pixel it runs fast. Reading that texture then in a different pass gives you the data you're looking for. It may be very sensitive though because even slight changes in the image change the avg luma a lot. You also need some sort of time delay. Look up the AmbientLight shader for some reference.
The administrator has disabled public write access.

How to find average luma within a frame 1 year 5 months ago #3

Correct me if I'm wrong, but isn't that extremely overly expensive? Calling that function on the pixel shader would result in 16 additional fetches per pixel, not to mention doing the math every pixel (which isn't that much, but just saying). It's better to run it once for a 1x1 texture, then have that texture fetched for each pixel, like Marty mentioned.

Also, you could try mixing something like said 200x200 sample with mipmaps, increasing the reach without as much cost. For 1080p, that would be 4th LOD with a 240x135 sample to cover the whole image, in a "naïve" way.

And can someone enlighten me about the difference between tex2D with an offset on its coords and tex2Doffset?
Last Edit: 1 year 5 months ago by ShoterXX.
The administrator has disabled public write access.

How to find average luma within a frame 1 year 5 months ago #4

hey guys,

so i've taken your advice, and now do my luma averaging in a shader pass to a 1x1 texture. here's what i have:
float SampleLuma(float4 position : SV_Position, float2 texcoord : TexCoord) : SV_Target {
	float luma = 0.0;

	luma += tex2D(SLogSampler, texcoord).a / (BUFFER_WIDTH * BUFFER_HEIGHT);

	float lastFrameLuma = tex2D(LumaSamplerLF, float2(0.5, 0.5)).x;

	return lerp(lastFrameLuma, luma, LumaChangeSpeed);
}

i'm also using another 1x1 texture to store the average luma from the previous frame, allowing for smoother transitions.

however, this doesn't actually work, because i don't have access to enough precision to actually do this. specifically, the line:
luma += tex2D(SLogSampler, texcoord).a / (BUFFER_WIDTH * BUFFER_HEIGHT);

would simply result in 0.0 on the right hand side, since BUFFER_WIDTH * BUFFER_HEIGHT is a huge number.

i know that ReShade can handle mipmaps, and i know how to set a texture to generate different mipmap levels, but i do not know the syntax to access them. the readme from the ReShade repo doesn't seem to go into it, so i'm at a loss as to how to do this.

alternatively, i can count the number of luma values, and if enough of them are brighter than a certain limit, i return a number to force the shader to use the daytime LUT, and vice versa.
The administrator has disabled public write access.

How to find average luma within a frame 1 year 5 months ago #5

What you are doing there is sample the first pixel of the back buffer and nothing more and then divide that by a huge number. That won't give you anything useful at all. What you want is sample across several pixels of the back buffer in a loop and average that. Forget the "texcoord" variable, that will always contain (0, 0) when you render to a 1x1 texture (since that's the whole point of rendering to a 1x1 texture).
for (int x = 0; x < SAMPLES; x++)
for (int y = 0; y < SAMPLES; y++)
{
    luma += tex2D(SLogSampler, float2(x, y) / SAMPLES);
}
luma /= SAMPLES * SAMPLES;

As for mipmaps, you use them via the tex2Dlod intrinsic. The backbuffer does not have mipmaps by default though. Only textures you explicitly defined in shader code to have mipmaps do. So that's of no use for you here.
Cheers, crosire =)
Last Edit: 1 year 5 months ago by crosire.
The administrator has disabled public write access.
The following user(s) said Thank You: moriz1

How to find average luma within a frame 1 year 5 months ago #6

When we meant 1x1 texure, we still meant to read more than one pixel. You need to do:
for x blah blah
for y blah blah
luma += tex2Dlod(SLogSampler, xy, mipmapLevel_integer).a; //Read x*y # of samples

luma /= x*y; //Not really, bcaz x and y will be floats, but I think you get it.

What you're trying to do in your example just results in a single pixel value of 1 at most being divided by billions. That's pretty much 0.

EDIT: I got ninja'd.
Last Edit: 1 year 5 months ago by ShoterXX.
The administrator has disabled public write access.
The following user(s) said Thank You: moriz1

How to find average luma within a frame 1 year 5 months ago #7

wait... so, if i call tex2D(A, texcoord), i am NOT using the texture coordinates of sampler A, but instead, i'm using the texture coordinates of the texture being written to?

if so, that explains EVERYTHING.

thanks guys.

as for mipmaps, SLogSampler is referencing an explicitly defined texture, so i should be able to assign mipmaps to it. something like level 6 should be small enough for nested for-loops to handle.
The administrator has disabled public write access.