From 1c828aee6f0d8a3e711d41927dad1f3f2ca8a051 Mon Sep 17 00:00:00 2001 From: Robert Beckebans Date: Wed, 22 Apr 2020 22:49:24 +0200 Subject: [PATCH] Added Blue Noise based Filmic Dithering by Timothy Lottes and Chromatic Aberration --- base/renderprogs/postprocess.ps.hlsl | 335 +++++++++++++++++++++++++-- neo/renderer/RenderBackend.cpp | 5 +- 2 files changed, 314 insertions(+), 26 deletions(-) diff --git a/base/renderprogs/postprocess.ps.hlsl b/base/renderprogs/postprocess.ps.hlsl index 06816d85..b0d0e81b 100644 --- a/base/renderprogs/postprocess.ps.hlsl +++ b/base/renderprogs/postprocess.ps.hlsl @@ -3,7 +3,8 @@ Doom 3 BFG Edition GPL Source Code Copyright (C) 1993-2012 id Software LLC, a ZeniMax Media company. -Copyright (C) 2015 Robert Beckebans +Copyright (C) 2015-2020 Robert Beckebans +Copyright (C) 2014 Timothy Lottes (AMD) This file is part of the Doom 3 BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code"). @@ -45,7 +46,9 @@ struct PS_OUT }; // *INDENT-ON* -#define USE_TECHNICOLOR 1 // [0 or 1] +#define USE_CHROMATIC_ABERRATION 1 + +#define USE_TECHNICOLOR 0 // [0 or 1] #define Technicolor_Amount 0.5 // [0.00 to 1.00] #define Technicolor_Power 4.0 // [0.00 to 8.00] @@ -53,11 +56,11 @@ struct PS_OUT #define Technicolor_GreenNegativeAmount 0.88 // [0.00 to 1.00] #define Technicolor_BlueNegativeAmount 0.88 // [0.00 to 1.00] -#define USE_VIBRANCE 1 +#define USE_VIBRANCE 0 #define Vibrance 0.5 // [-1.00 to 1.00] #define Vibrance_RGB_Balance float3( 1.0, 1.0, 1.0 ) -#define USE_FILMGRAIN 1 +#define USE_DITHERING 1 float3 overlay( float3 a, float3 b ) { @@ -111,29 +114,307 @@ void VibrancePass( inout float4 color ) } -void FilmgrainPass( inout float4 color ) +// CHROMATIC ABBERATION + +float2 BarrelDistortion( float2 xy, float amount ) { - float4 jitterTC = ( fragment.position * rpScreenCorrectionFactor ) + rpJitterTexOffset; - //float4 jitterTC = ( fragment.position * ( 1.0 / 128.0 ) ) + rpJitterTexOffset; - //float2 jitterTC = fragment.position.xy * 2.0; - //jitterTC.x *= rpWindowCoord.y / rpWindowCoord.x; + float2 cc = xy - 0.5; + float dist = dot2( cc, cc ); - float4 noiseColor = tex2D( samp1, fragment.position.xy + jitterTC.xy ); - float Y = noiseColor.r; - //float Y = dot( LUMINANCE_VECTOR, noiseColor ); - //noiseColor.rgb = float3( Y, Y, Y ); - - float exposureFactor = 1.0; - exposureFactor = sqrt( exposureFactor ); - const float noiseIntensity = 1.7; //rpScreenCorrectionFactor.z; - - float t = lerp( 3.5 * noiseIntensity, 1.13 * noiseIntensity, exposureFactor ); - color.rgb = overlay( color.rgb, lerp( float3( 0.5 ), noiseColor.rgb, t ) ); - - //color.rgb = noiseColor.rgb; - //color.rgb = lerp( color.rgb, noiseColor.rgb, 0.3 ); + return xy + cc * dist * amount; } +float Linterp( float t ) +{ + return saturate( 1.0 - abs( 2.0 * t - 1.0 ) ); +} + +float Remap( float t, float a, float b ) +{ + return saturate( ( t - a ) / ( b - a ) ); +} + +float3 SpectrumOffset( float t ) +{ + float lo = step( t, 0.5 ); + float hi = 1.0 - lo; + float w = Linterp( Remap( t, 1.0 / 6.0, 5.0 / 6.0 ) ); + float3 ret = float3( lo, 1.0, hi ) * float3( 1.0 - w, w, 1.0 - w ); + + return pow( ret, float3( 1.0 / 2.2 ) ); +} + +void ChromaticAberrationPass( inout float4 color ) +{ + float amount = 0.1; //color.a * 1.0; //rpUser0.x; + + float3 sum = float3( 0.0 ); + float3 sumColor = float3( 0.0 ); + + //float samples = rpOverbright.x; + float samples = 12.0; // * 2; + + for( float i = 0.0; i < samples; i = i + 1.0 ) + { + //float t = ( ( i / ( samples - 1.0 ) ) - 0.5 ); + float t = ( i / ( samples - 1.0 ) ); + //float t = log( i / ( samples - 1.0 ) ); + + float3 so = SpectrumOffset( t ); + + sum += so.xyz; + sumColor += so * tex2D( samp0, BarrelDistortion( fragment.texcoord0, ( 0.5 * amount * t ) ) ).rgb; + } + + color.rgb = ( sumColor / sum ); + //color.rgb = lerp(color.rgb, (sumColor / sum), Technicolor_Amount); +} + + + +// https://gpuopen.com/vdr-follow-up-fine-art-of-film-grain/ + +// +// TEMPORAL DITHERING TEST IN LINEAR +// + +// +// This is biased (saturates + adds contrast) because dithering done in non-linear space. +// + +// Shows proper dithering of a signal (overlapping of dither between bands). +// Results in about a 1-stop improvement in dynamic range over conventional dither +// which does not overlap dither across bands +// (try "#define WIDE 0.5" to see the difference below). +// +// This would work a lot better with a proper random number generator (flicker etc is bad). +// Sorry there is a limit to what can be done easily in shadertoy. +// +// Proper dithering algorithm, +// +// color = floor(color * steps + noise) * (1.0/(steps-1.0)) +// +// Where, +// +// color ... output color {0 to 1} +// noise ... random number between {-1 to 1} +// steps ... quantization steps, ie 8-bit = 256 +// +// The noise in this case is shaped by a high pass filter. +// This is to produce a better quality temporal dither. + +// Scale the width of the dither + +//----------------------------------------------------------------------- + +float Linear1( float c ) +{ + return ( c <= 0.04045 ) ? c / 12.92 : pow( ( c + 0.055 ) / 1.055, 2.4 ); +} + +float3 Linear3( float3 c ) +{ + return float3( Linear1( c.r ), Linear1( c.g ), Linear1( c.b ) ); +} + +float Srgb1( float c ) +{ + return ( c < 0.0031308 ? c * 12.92 : 1.055 * pow( c, 0.41666 ) - 0.055 ); +} + +float3 Srgb3( float3 c ) +{ + return float3( Srgb1( c.r ), Srgb1( c.g ), Srgb1( c.b ) ); +} + +float3 photoLuma = float3( 0.2126, 0.7152, 0.0722 ); +float PhotoLuma( float3 c ) +{ + return dot( c, photoLuma ); +} + +//note: works for structured patterns too +// [0;1[ +float RemapNoiseTriErp( const float v ) +{ + float r2 = 0.5 * v; + float f1 = sqrt( r2 ); + float f2 = 1.0 - sqrt( r2 - 0.25 ); + return ( v < 0.5 ) ? f1 : f2; +} + +#if 1 +float Noise( float2 n, float x ) +{ + // golden ratio + n += x;// * 1.61803398875; + return fract( sin( dot( n.xy, float2( 12.9898, 78.233 ) ) ) * 43758.5453 ) * 2.0 - 1.0; +} + +#else + +//note: returns [-intensity;intensity[, magnitude of 2x intensity +//note: from "NEXT GENERATION POST PROCESSING IN CALL OF DUTY: ADVANCED WARFARE" +// http://advances.realtimerendering.com/s2014/index.html +//float InterleavedGradientNoise( vec2 uv ) +float Noise( float2 uv, float x ) +{ + // RB: golden ratio + uv += x;// * 1.61803398875; + + const float3 magic = vec3( 0.06711056, 0.00583715, 52.9829189 ); + float rnd = fract( magic.z * fract( dot( uv, magic.xy ) ) ); + + //rnd = RemapNoiseTriErp(rnd) * 2.0 - 0.5; + + return rnd; +} +#endif + +// Step 1 in generation of the dither source texture. +float Step1( float2 uv, float n ) +{ + float a = 1.0, b = 2.0, c = -12.0, t = 1.0; + return ( 1.0 / ( a * 4.0 + b * 4.0 - c ) ) * ( + Noise( uv + float2( -1.0, -1.0 ) * t, n ) * a + + Noise( uv + float2( 0.0, -1.0 ) * t, n ) * b + + Noise( uv + float2( 1.0, -1.0 ) * t, n ) * a + + Noise( uv + float2( -1.0, 0.0 ) * t, n ) * b + + Noise( uv + float2( 0.0, 0.0 ) * t, n ) * c + + Noise( uv + float2( 1.0, 0.0 ) * t, n ) * b + + Noise( uv + float2( -1.0, 1.0 ) * t, n ) * a + + Noise( uv + float2( 0.0, 1.0 ) * t, n ) * b + + Noise( uv + float2( 1.0, 1.0 ) * t, n ) * a + + 0.0 ); +} + +// Step 2 in generation of the dither source texture. +float Step2( float2 uv, float n ) +{ + float a = 1.0, b = 2.0, c = -2.0, t = 1.0; + return ( 1.0 / ( a * 4.0 + b * 4.0 - c ) ) * ( + Step1( uv + float2( -1.0, -1.0 ) * t, n ) * a + + Step1( uv + float2( 0.0, -1.0 ) * t, n ) * b + + Step1( uv + float2( 1.0, -1.0 ) * t, n ) * a + + Step1( uv + float2( -1.0, 0.0 ) * t, n ) * b + + Step1( uv + float2( 0.0, 0.0 ) * t, n ) * c + + Step1( uv + float2( 1.0, 0.0 ) * t, n ) * b + + Step1( uv + float2( -1.0, 1.0 ) * t, n ) * a + + Step1( uv + float2( 0.0, 1.0 ) * t, n ) * b + + Step1( uv + float2( 1.0, 1.0 ) * t, n ) * a + + 0.0 ); +} + +// Used for stills. +float3 Step3( float2 uv ) +{ + float a = Step2( uv, 0.07 ); + float b = Step2( uv, 0.11 ); + float c = Step2( uv, 0.13 ); +#if 0 + // Monochrome can look better on stills. + return float3( a ); +#else + return float3( a, b, c ); +#endif +} + +// Used for temporal dither. +float3 Step3T( float2 uv ) +{ + float a = Step2( uv, 0.07 * fract( rpJitterTexOffset.z ) ); + float b = Step2( uv, 0.11 * fract( rpJitterTexOffset.z ) ); + float c = Step2( uv, 0.13 * fract( rpJitterTexOffset.z ) ); + return float3( a, b, c ); +} + +#define STEPS 12.0 + +void DitheringPass( inout float4 fragColor ) +{ + float2 uv = fragment.position.xy; + float2 uv2 = fragment.texcoord0; + //float2 uv3 = float2( uv2.x, 1.0 - uv2.y ); + + float3 color = fragColor.rgb; + //float3 color = tex2D(samp0, uv2).rgb; + +#if 0 +// BOTTOM: Show bands. + if( uv2.y >= 0.975 ) + { + color = float3( uv2.x ); + color = floor( color * STEPS + Step3( uv ) * 4.0 ) * ( 1.0 / ( STEPS - 1.0 ) ); + } + else if( uv2.y >= 0.95 ) + { + color = float3( uv2.x ); + color = floor( color * STEPS ) * ( 1.0 / ( STEPS - 1.0 ) ); + } + else if( uv2.y >= 0.925 ) + { + color = float3( uv2.x ); + color = floor( color * STEPS + Step3T( uv ) * 4.0 ) * ( 1.0 / ( STEPS - 1.0 ) ); + } + // TOP: Show dither texture. + else if( uv2.y >= 0.9 ) + { + color = Step3( uv ) * 1.0 + 0.5; + } + else +#endif + { + color = Linear3( color ); + + // Add grain in linear space. +#if 0 + // Slow more correct solutions. +#if 1 + // Too expensive. + // Helps understand the fast solutions. + float3 amount = Linear3( Srgb3( color ) + ( 4.0 / STEPS ) ) - color; +#else + // Less too expensive. + float luma = PhotoLuma( color ); + + // Implement this as a texture lookup table. + float amount = Linear1( Srgb1( luma ) + ( 4.0 / STEPS ) ) - luma; +#endif + +#else + // Fast solutions. +#if 1 + // Hack 1 (fastest). + // For HDR need saturate() around luma. + float luma = PhotoLuma( color ); + float amount = mix( + Linear1( 4.0 / STEPS ), + Linear1( ( 4.0 / STEPS ) + 1.0 ) - 1.0, + luma ); +#else + // Hack 2 (slower?). + // For HDR need saturate() around color in mix(). + float3 amount = mix( + float3( Linear1( 4.0 / STEPS ) ), + float3( Linear1( ( 4.0 / STEPS ) + 1.0 ) - 1.0 ), + color ); +#endif + +#endif + color += Step3T( uv ) * amount; + + // The following represents hardware linear->sRGB xform + // which happens on sRGB formatted render targets, + // except using a lot less bits/pixel. + color = max( float3( 0.0 ), color ); + color = Srgb3( color ); + color = floor( color * STEPS ) * ( 1.0 / ( STEPS - 1.0 ) ); + } + + fragColor.rgb = color; +} + + void main( PS_IN fragment, out PS_OUT result ) { @@ -142,6 +423,10 @@ void main( PS_IN fragment, out PS_OUT result ) // base color with tone mapping and other post processing applied float4 color = tex2D( samp0, tCoords ); +#if USE_CHROMATIC_ABERRATION + ChromaticAberrationPass( color ); +#endif + #if USE_TECHNICOLOR TechnicolorPass( color ); #endif @@ -150,8 +435,8 @@ void main( PS_IN fragment, out PS_OUT result ) VibrancePass( color ); #endif -#if USE_FILMGRAIN - FilmgrainPass( color ); +#if USE_DITHERING + DitheringPass( color ); #endif result.color = color; diff --git a/neo/renderer/RenderBackend.cpp b/neo/renderer/RenderBackend.cpp index 3fc38371..00356701 100644 --- a/neo/renderer/RenderBackend.cpp +++ b/neo/renderer/RenderBackend.cpp @@ -5801,6 +5801,7 @@ void idRenderBackend::MotionBlur() GL_SelectTexture( 0 ); globalImages->currentRenderImage->Bind(); + GL_SelectTexture( 1 ); globalImages->currentDepthImage->Bind(); @@ -6066,13 +6067,15 @@ void idRenderBackend::PostProcess( const void* data ) { jitterTexOffset[0] = ( rand() & 255 ) / 255.0; jitterTexOffset[1] = ( rand() & 255 ) / 255.0; + jitterTexOffset[2] = Sys_Milliseconds() / 1000.0f; } else { jitterTexOffset[0] = 0; jitterTexOffset[1] = 0; + jitterTexOffset[2] = 0.0f; } - jitterTexOffset[2] = 0.0f; + jitterTexOffset[3] = 0.0f; SetFragmentParm( RENDERPARM_JITTERTEXOFFSET, jitterTexOffset ); // rpJitterTexOffset