Using RenderDoc to inspect a frame from the game, I discovered that before the final render texture is generated, there's a 16 bit per channel floating point render texture generated, which is then passed through a pixel shader to map the values to an 8 bit colorspace, with no correction, leading to the primitive-looking saturated overbrights.
The shader responsible is called Hidden/BlitCopy:
// HLSL function stub generated
SamplerState sampler0 : register(s0); // can't disambiguate
//SamplerComparisonState sampler0 : register(s0); // can't disambiguate
Texture2D<float4> texture0 : register(t0);
cbuffer cbuffer0 : register(b0) {
float4 cb0_v0 : packoffset(c0.x);
float4 cb0_v1 : packoffset(c1.x);
float4 cb0_v2 : packoffset(c2.x);
float4 cb0_v3 : packoffset(c3.x);
};
struct InputStruct {
float4 Position : SV_Position;
float2 param1 : TEXCOORD;
};
struct OutputStruct {
float4 Target0 : SV_Target0;
};
OutputStruct EditedShaderPS(in InputStruct IN)
{
OutputStruct OUT = (OutputStruct)0;
// ...
return OUT;
}
If you simply change it to:
// HLSL function stub generated
SamplerState sampler0 : register(s0); // can't disambiguate
//SamplerComparisonState sampler0 : register(s0); // can't disambiguate
Texture2D<float4> texture0 : register(t0);
cbuffer cbuffer0 : register(b0) {
float4 cb0_v0 : packoffset(c0.x);
float4 cb0_v1 : packoffset(c1.x);
float4 cb0_v2 : packoffset(c2.x);
float4 cb0_v3 : packoffset(c3.x);
};
struct InputStruct {
float4 Position : SV_Position;
float2 param1 : TEXCOORD;
};
struct OutputStruct {
float4 Target0 : SV_Target0;
};
OutputStruct EditedShaderPS(in InputStruct IN)
{
OutputStruct OUT = (OutputStruct)0;
float4 hdr = texture0.Sample(sampler0, IN.param1);
// Preserve the existing Unity blit multiplier
hdr *= cb0_v3;
// Simple Reinhard tone mapping
float3 mapped = hdr.rgb / (hdr.rgb + 1.0);
OUT.Target0 = float4(mapped, hdr.a);
return OUT;
}
The overbrights are clamped and it looks a hundred times better.
Another option is an ACES tonemap:
// HLSL function stub generated
SamplerState sampler0 : register(s0);
Texture2D<float4> texture0 : register(t0);
cbuffer cbuffer0 : register(b0) {
float4 cb0_v0 : packoffset(c0.x);
float4 cb0_v1 : packoffset(c1.x);
float4 cb0_v2 : packoffset(c2.x);
float4 cb0_v3 : packoffset(c3.x);
};
struct InputStruct {
float4 Position : SV_Position;
float2 param1 : TEXCOORD;
};
struct OutputStruct {
float4 Target0 : SV_Target0;
};
// Add this function here:
float3 ACESFilm(float3 x)
{
float a = 2.51;
float b = 0.03;
float c = 2.43;
float d = 0.59;
float e = 0.14;
return saturate((x*(a*x+b))/(x*(c*x+d)+e));
}
// Replace the entire EditedShaderPS function:
OutputStruct EditedShaderPS(in InputStruct IN)
{
OutputStruct OUT = (OutputStruct)0;
float4 hdr = texture0.Sample(sampler0, IN.param1);
// Keep Unity's original multiplier
hdr *= cb0_v3;
float3 mapped = ACESFilm(hdr.rgb);
OUT.Target0 = float4(mapped, hdr.a);
return OUT;
}
Here's some comparisons generated in Renderdoc (I don't have the knowledge to implement this into the game)
ORIGINAL
WITH CLAMPING
WITH ACES TONEMAP
If anyone can make this into a plugin or mod of some sort, it could make the game look incredible.
The shader responsible is called Hidden/BlitCopy:
// HLSL function stub generated
SamplerState sampler0 : register(s0); // can't disambiguate
//SamplerComparisonState sampler0 : register(s0); // can't disambiguate
Texture2D<float4> texture0 : register(t0);
cbuffer cbuffer0 : register(b0) {
float4 cb0_v0 : packoffset(c0.x);
float4 cb0_v1 : packoffset(c1.x);
float4 cb0_v2 : packoffset(c2.x);
float4 cb0_v3 : packoffset(c3.x);
};
struct InputStruct {
float4 Position : SV_Position;
float2 param1 : TEXCOORD;
};
struct OutputStruct {
float4 Target0 : SV_Target0;
};
OutputStruct EditedShaderPS(in InputStruct IN)
{
OutputStruct OUT = (OutputStruct)0;
// ...
return OUT;
}
If you simply change it to:
// HLSL function stub generated
SamplerState sampler0 : register(s0); // can't disambiguate
//SamplerComparisonState sampler0 : register(s0); // can't disambiguate
Texture2D<float4> texture0 : register(t0);
cbuffer cbuffer0 : register(b0) {
float4 cb0_v0 : packoffset(c0.x);
float4 cb0_v1 : packoffset(c1.x);
float4 cb0_v2 : packoffset(c2.x);
float4 cb0_v3 : packoffset(c3.x);
};
struct InputStruct {
float4 Position : SV_Position;
float2 param1 : TEXCOORD;
};
struct OutputStruct {
float4 Target0 : SV_Target0;
};
OutputStruct EditedShaderPS(in InputStruct IN)
{
OutputStruct OUT = (OutputStruct)0;
float4 hdr = texture0.Sample(sampler0, IN.param1);
// Preserve the existing Unity blit multiplier
hdr *= cb0_v3;
// Simple Reinhard tone mapping
float3 mapped = hdr.rgb / (hdr.rgb + 1.0);
OUT.Target0 = float4(mapped, hdr.a);
return OUT;
}
The overbrights are clamped and it looks a hundred times better.
Another option is an ACES tonemap:
// HLSL function stub generated
SamplerState sampler0 : register(s0);
Texture2D<float4> texture0 : register(t0);
cbuffer cbuffer0 : register(b0) {
float4 cb0_v0 : packoffset(c0.x);
float4 cb0_v1 : packoffset(c1.x);
float4 cb0_v2 : packoffset(c2.x);
float4 cb0_v3 : packoffset(c3.x);
};
struct InputStruct {
float4 Position : SV_Position;
float2 param1 : TEXCOORD;
};
struct OutputStruct {
float4 Target0 : SV_Target0;
};
// Add this function here:
float3 ACESFilm(float3 x)
{
float a = 2.51;
float b = 0.03;
float c = 2.43;
float d = 0.59;
float e = 0.14;
return saturate((x*(a*x+b))/(x*(c*x+d)+e));
}
// Replace the entire EditedShaderPS function:
OutputStruct EditedShaderPS(in InputStruct IN)
{
OutputStruct OUT = (OutputStruct)0;
float4 hdr = texture0.Sample(sampler0, IN.param1);
// Keep Unity's original multiplier
hdr *= cb0_v3;
float3 mapped = ACESFilm(hdr.rgb);
OUT.Target0 = float4(mapped, hdr.a);
return OUT;
}
Here's some comparisons generated in Renderdoc (I don't have the knowledge to implement this into the game)
ORIGINAL
WITH CLAMPING
WITH ACES TONEMAP
If anyone can make this into a plugin or mod of some sort, it could make the game look incredible.