Welcome, Guest.
Username: Password: Remember me

TOPIC: Adaptive Color Grading

Adaptive Color Grading 1 year 7 months ago #1

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 administrator has disabled public write access.
The following user(s) said Thank You: crosire, SpinelessJelly, v00d00m4n, andrew, XIIICaesar, Ryukou36, Gar Stazi, @rnkrnt

Adaptive Color Grading 1 year 6 months ago #2

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.
The administrator has disabled public write access.

Adaptive Color Grading 1 year 6 months ago #3

Format = R8 was done on purpose.

RGBA8 has 4 color channels, which is unnecessary for storing luma info, since luma only uses one channel.
The administrator has disabled public write access.

Adaptive Color Grading 1 year 6 months ago #4

Sounds and looks awesome man!

Could you provide the 4D LUT texture for me?
The administrator has disabled public write access.

Adaptive Color Grading 1 year 5 months ago #5

???
The administrator has disabled public write access.

Adaptive Color Grading 1 year 5 months ago #6

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.
The administrator has disabled public write access.

Adaptive Color Grading 1 year 4 months ago #7

Can I use the ones that comes with ReShade by default?
The administrator has disabled public write access.

Adaptive Color Grading 1 year 4 months ago #8

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.
The administrator has disabled public write access.

Adaptive Color Grading 1 year 3 months ago #9

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: 1 year 3 months ago by magicart87.
The administrator has disabled public write access.

Adaptive Color Grading 1 year 3 months ago #10

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.
The administrator has disabled public write access.

Adaptive Color Grading 1 year 3 months ago #11

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: 1 year 3 months ago by magicart87.
The administrator has disabled public write access.