Adaptive Color Grading

  • moriz1
  • Topic Author
More
7 years 2 months ago #1 by moriz1 Adaptive Color Grading was created by moriz1
i've been thinking about the issue with color grading in video games, where one preset might look good in one situation, but end up looking terrible in another. the most obvious is changes in scene brightness, where one preset might work well when it is brightly lit, but falls apart when brightness falls.

so, i hacked together a shader that uses two LUTs, and smoothly lerps between the two based on screen luma. one LUT is for bright scenes, the other for night scenes. it can also selectively apply highlights to bright objects when the scene is sufficiently dark, preventing things like torches and lights from being shaded unrealistically in night scenes.

i elected to use two LUTs and lerp between them over a 4D LUT, because i want it to be easy for users to apply their own LUTs. a 4D LUT is significantly harder to set up, and isn't as configurable once the game is running.

the code:
/**
 * Adaptive Color Grading
 * Runs two LUTs simultaneously, smoothly lerping between them based on luma.
 * By moriz1
 * Original LUT shader by Marty McFly
 */

#ifndef fLUT_TextureDay
	#define fLUT_TextureDay "lutDAY.png"
#endif
#ifndef fLUT_TextureNight
	#define fLUT_TextureNight "lutNIGHT.png"
#endif
#ifndef fLUT_TileSizeXY
	#define fLUT_TileSizeXY 32
#endif
#ifndef fLUT_TileAmount
	#define fLUT_TileAmount 32
#endif

uniform bool DebugLuma <
    ui_label = "Show Luma Debug Bars";
	ui_tooltip = "Draws debug bars on top left of screen";
> = false;

uniform bool DebugLumaOutput <
    ui_label = "Show Luma Output";
	ui_tooltip = "Black/White blurry mode!";
> = false;

uniform bool DebugLumaOutputHQ <
    ui_label = "Show Luma Output at Native Resolution";
	ui_tooltip = "Black/White mode!";
> = false;

uniform bool EnableHighlightsInDarkScenes <
	ui_label = "Enable Highlights";
    ui_tooltip = "Add highlights to bright objects when in dark scenes";
> = true;

uniform bool DebugHighlights <
    ui_label = "Show Debug Highlights";
	ui_tooltip = "If any highlights are in the frame, this colours them magenta";
> = false;

uniform float LumaChangeSpeed <
	ui_label = "Adaptation Speed";
	ui_type = "drag";
	ui_min = 0.0; ui_max = 1.0;
> = 0.05;

uniform float LumaHigh <
	ui_label = "Luma Max Threshold";
	ui_tooltip = "Luma above this level uses full Daytime LUT\nSet higher than Min Threshold";
	ui_type = "drag";
	ui_min = 0.0; ui_max = 1.0;
> = 0.75;

uniform float LumaLow <
	ui_label = "Luma Min Threshold";
	ui_tooltip = "Luma below this level uses full NightTime LUT\nSet lower than Max Threshold";
	ui_type = "drag";
	ui_min = 0.0; ui_max = 1.0;
> = 0.2;

uniform float AmbientHighlightThreshold <
	ui_label = "Low Luma Highlight Start";
	ui_tooltip = "If average luma falls below this limit, start adding highlights\nSimulates HDR look in low light";
	ui_type = "drag";
	ui_min = 0.0; ui_max = 1.0;
> = 0.5;

uniform float HighlightThreshold <
	ui_label = "Minimum Luma For Highlights";
	ui_tooltip = "Any luma value above this will have highlights\nSimulates HDR look in low light";
	ui_type = "drag";
	ui_min = 0.0; ui_max = 1.0;
> = 0.5;

uniform float HighlightMaxThreshold <
	ui_label = "Max Luma For Highlights";
	ui_tooltip = "Highlights reach maximum strength at this luma value\nSimulates HDR look in low light";
	ui_type = "drag";
	ui_min = 0.0; ui_max = 1.0;
> = 0.8;

#include "ReShade.fxh"

texture LumaInputTex { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R8; MipLevels = 6; };
sampler LumaInputSampler { Texture = LumaInputTex; MipLODBias = 6.0f; };
sampler LumaInputSamplerHQ { Texture = LumaInputTex; };

texture LumaTex { Width = 1; Height = 1; Format = R8; };
sampler LumaSampler { Texture = LumaTex; };

texture LumaTexLF { Width = 1; Height = 1; Format = R8; };
sampler LumaSamplerLF { Texture = LumaTexLF; };

texture texLUTDay < source = fLUT_TextureDay; > { Width = fLUT_TileSizeXY*fLUT_TileAmount; Height = fLUT_TileSizeXY; Format = RGBA8; };
sampler	SamplerLUTDay	{ Texture = texLUTDay; };

texture texLUTNight < source = fLUT_TextureNight; > { Width = fLUT_TileSizeXY*fLUT_TileAmount; Height = fLUT_TileSizeXY; Format = RGBA8; };
sampler	SamplerLUTNight	{ Texture = texLUTNight; };

float SampleLuma(float4 position : SV_Position, float2 texcoord : TexCoord) : SV_Target {
	float luma = 0.0;

	int width = BUFFER_WIDTH / 64;
	int height = BUFFER_HEIGHT / 64;

	for (int i = width/3; i < 2*width/3; i++) {
		for (int j = height/3; j < 2*height/3; j++) {
			luma += tex2Dlod(LumaInputSampler, float4(i, j, 0, 6)).x;
		}
	}

	luma /= (width * 1/3) * (height * 1/3);

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

	return lerp(lastFrameLuma, luma, LumaChangeSpeed);
}

float LumaInput(float4 position : SV_Position, float2 texcoord : TexCoord) : SV_Target {
	float3 color = tex2D(ReShade::BackBuffer, texcoord).xyz;
	
	return pow((color.r*2 + color.b + color.g*3) / 6, 1/2.2);
}

float3 ApplyLUT(float4 position : SV_Position, float2 texcoord : TexCoord) : SV_Target {
	float3 color = tex2D(ReShade::BackBuffer, texcoord.xy).rgb;
	float lumaVal = tex2D(LumaSampler, float2(0.5, 0.5)).x;
	float highlightLuma = tex2D(LumaInputSamplerHQ, texcoord.xy).x;

	if (DebugLumaOutputHQ) {
		return highlightLuma;
	}
	else if (DebugLumaOutput) {
		return lumaVal;
	}

	if (DebugLuma) {
		if (texcoord.y <= 0.01 && texcoord.x <= 0.01) {
			return lumaVal;
		}
		if (texcoord.y <= 0.01 && texcoord.x > 0.01 && texcoord.x <= 0.02) {
			if (lumaVal > LumaHigh) {
				return float3(1.0, 1.0, 1.0);
			}
			else {
				return float3(0.0, 0.0, 0.0);
			}
		}
		if (texcoord.y <= 0.01 && texcoord.x > 0.02 && texcoord.x <= 0.03) {
			if (lumaVal <= LumaHigh && lumaVal >= LumaLow) {
				return float3(1.0, 1.0, 1.0);
			}
			else {
				return float3(0.0, 0.0, 0.0);
			}
		}
		if (texcoord.y <= 0.01 && texcoord.x > 0.03 && texcoord.x <= 0.04) {
			if (lumaVal < LumaLow) {
				return float3(1.0, 1.0, 1.0);
			}
			else {
				return float3(0.0, 0.0, 0.0);
			}
		}
	}

	float2 texelsize = 1.0 / fLUT_TileSizeXY;
	texelsize.x /= fLUT_TileAmount;

	float3 lutcoord = float3((color.xy*fLUT_TileSizeXY-color.xy+0.5)*texelsize.xy,color.z*fLUT_TileSizeXY-color.z);
	float lerpfact = frac(lutcoord.z);

	lutcoord.x += (lutcoord.z-lerpfact)*texelsize.y;
	
	float3 color1 = lerp(tex2D(SamplerLUTDay, lutcoord.xy).xyz, tex2D(SamplerLUTDay, float2(lutcoord.x+texelsize.y,lutcoord.y)).xyz,lerpfact);
	float3 color2 = lerp(tex2D(SamplerLUTNight, lutcoord.xy).xyz, tex2D(SamplerLUTNight, float2(lutcoord.x+texelsize.y,lutcoord.y)).xyz,lerpfact);	

	float range = (lumaVal - LumaLow)/(LumaHigh - LumaLow);

	if (lumaVal > LumaHigh) {
		color.xyz = color1.xyz;
	}
	else if (lumaVal < LumaLow) {
		color.xyz = color2.xyz;
	}
	else {
		color.xyz = lerp(color2.xyz, color1.xyz, range);
	}

	float3 lutcoord2 = float3((color.xy*fLUT_TileSizeXY-color.xy+0.5)*texelsize.xy,color.z*fLUT_TileSizeXY-color.z);
	float lerpfact2 = frac(lutcoord2.z);

	lutcoord2.x += (lutcoord2.z-lerpfact2)*texelsize.y;
	
	float3 highlightColor = lerp(tex2D(SamplerLUTDay, lutcoord2.xy).xyz, tex2D(SamplerLUTDay, float2(lutcoord2.x+texelsize.y,lutcoord2.y)).xyz,lerpfact2);

	//apply highlights
	if (EnableHighlightsInDarkScenes) {
		if (lumaVal < AmbientHighlightThreshold && highlightLuma > HighlightThreshold) {
			float range = saturate((highlightLuma - HighlightThreshold)/(HighlightMaxThreshold - HighlightThreshold));

			if (DebugHighlights) {
				color.xyz = lerp(color.xyz, float3(1.0, 0.0, 1.0), range);
				
				if (range >= 1.0) {
					color.xyz = float3(1.0, 0.0, 0.0);
				}
			}

			color.xyz = lerp(color.xyz, highlightColor.xyz, range);
		}
	}

	return color;
}

float SampleLumaLF(float4 position : SV_Position, float2 texcoord: TexCoord) : SV_Target {
	return tex2D(LumaSampler, float2(0.5, 0.5)).x;
}

technique AdaptiveColorGrading {
	pass Input {
		VertexShader = PostProcessVS;
		PixelShader = LumaInput;
		RenderTarget = LumaInputTex
	;
	}
	pass StoreLuma {
		VertexShader = PostProcessVS;
		PixelShader = SampleLuma;
		RenderTarget = LumaTex;
	}
	pass Apply_LUT {
		VertexShader = PostProcessVS;
		PixelShader = ApplyLUT;
	}
	pass StoreLumaLF {
		VertexShader = PostProcessVS;
		PixelShader = SampleLumaLF;
		RenderTarget = LumaTexLF;
	}
}

demonstration:


if anyone see any screwups on my part, please let me know. for one thing, i'm not entirely sure if having the luma delay is necessary. i also don't know how to get the highlights to turn off smoothly (you can see near the beginning of the vid, where the highlights cut off abruptly once screen luma rises above a certain point).
The following user(s) said Thank You: crosire, SpinelessJelly, v00d00m4n, andrew, XIIICaesar, Ryukou36, Gar Stazi, @rnkrnt

Please Log in or Create an account to join the conversation.

  • XIIICaesar
More
7 years 1 month ago #2 by XIIICaesar Replied by XIIICaesar on topic Adaptive Color Grading
I just did some testing in Fallout 4 with l00ping's Psy-Fi prest from No Man's Sky. He use TileAmount 5 with a 5 layer LUT through TuningPalette in ReShade 2.0.3f1 to have the LUT change it's color grade at will in accordance with current scene brightness. You've acheived that with this. It works identical dude and a virtual replacement. Also, in the shader code you've posted Format = R8 in 3 places. I changed that to Format = RGBA8 & it works great. Now I just need to try to attempt to tweak your shader to allow for 5 LUTs for a more gradual color change & it's a contender for TuningPalette Lol.

Please Log in or Create an account to join the conversation.

  • moriz1
  • Topic Author
More
7 years 1 month ago #3 by moriz1 Replied by moriz1 on topic Adaptive Color Grading
Format = R8 was done on purpose.

RGBA8 has 4 color channels, which is unnecessary for storing luma info, since luma only uses one channel.

Please Log in or Create an account to join the conversation.

  • F D B
More
7 years 1 month ago #4 by F D B Replied by F D B on topic Adaptive Color Grading
Sounds and looks awesome man!

Could you provide the 4D LUT texture for me?

Please Log in or Create an account to join the conversation.

  • F D B
More
7 years 3 weeks ago #5 by F D B Replied by F D B on topic Adaptive Color Grading
???

Please Log in or Create an account to join the conversation.

  • moriz1
  • Topic Author
More
7 years 5 days ago #6 by moriz1 Replied by moriz1 on topic Adaptive Color Grading
oh hello. sorry, i haven't checked back in a while.

there isn't a 4D LUT. instead, just make two 3D LUTs, one for bright scenes, and the other for dark scenes. name them lutDAY.png and lutNight.png respectively.

Please Log in or Create an account to join the conversation.

  • F D B
More
6 years 11 months ago #7 by F D B Replied by F D B on topic Adaptive Color Grading
Can I use the ones that comes with ReShade by default?

Please Log in or Create an account to join the conversation.

  • F D B
More
6 years 11 months ago #8 by F D B Replied by F D B on topic Adaptive Color Grading
Can I use this LUT (original) as base to be edited for both DAY and NIGHT?



It's the one that came in a previous ReShade version.

Please Log in or Create an account to join the conversation.

  • magicart87
More
6 years 11 months ago - 6 years 10 months ago #9 by magicart87 Replied by magicart87 on topic Adaptive Color Grading
So It seems I'm not able to get this to work. Not for a lack of trying and fighting the values; it's just that my poorly designed game doesn't use a large enough Luma drop when shifting from daytime to nighttime to trigger the lut lerps. My reason for wanting to use this effect was to make a more obvious day to night change. I can't seem to get it to work however. Any ideas?

Because my game uses mostly colorcast (to ill effect) to hint at the changes from day to night (orange = day/blue = night) would it be possible to reference chroma as well?
Last edit: 6 years 10 months ago by magicart87.

Please Log in or Create an account to join the conversation.

  • moriz1
  • Topic Author
More
6 years 10 months ago #10 by moriz1 Replied by moriz1 on topic Adaptive Color Grading

F D B wrote: Can I use this LUT (original) as base to be edited for both DAY and NIGHT?



It's the one that came in a previous ReShade version.


yes you can.

Please Log in or Create an account to join the conversation.

  • magicart87
More
6 years 10 months ago - 6 years 10 months ago #11 by magicart87 Replied by magicart87 on topic Adaptive Color Grading
OK so I figured out why this isn't working for me. It doesn't launch. I can edit it in config mode but when I go to run it in performance mode the shader doesn't run. Toggling Reshade on/off has no effect. Is this an issue with the code or something buggy with Reshade?
Last edit: 6 years 10 months ago by magicart87.

Please Log in or Create an account to join the conversation.

  • eveclear
More
3 years 11 months ago #12 by eveclear Replied by eveclear on topic Adaptive Color Grading
your shader is incredible, I was able to correct the color of most of RE6, but there is a part of the game that is dark with another type of filter that I cannot correct with only light levels ...
it would be just perfect if it had something like "adaptive color grading 2" that activates depending on whether the scene is too green or too blue, it would solve the case of games where in certain scenarios it has a green filter having no relation to brightness, so I would use both versions and could make color correction to ANY GAME.

Please Log in or Create an account to join the conversation.

We use cookies
We use cookies on our website. Some of them are essential for the operation of the forum. You can decide for yourself whether you want to allow cookies or not. Please note that if you reject them, you may not be able to use all the functionalities of the site.