Tweaked PSX dithering

This commit is contained in:
Robert Beckebans 2023-12-30 18:43:09 +01:00
parent fd5b25dc59
commit 779f38d84c
4 changed files with 69 additions and 160 deletions

View file

@ -72,8 +72,6 @@ struct PS_OUT
#define Dithering_QuantizationSteps 16.0 // 8.0 = 2 ^ 3 quantization bits
#define Dithering_NoiseBoost 1.0
#define Dithering_Wide 1.0
#define DITHER_IN_LINEAR_SPACE 0
#define DITHER_GENERATE_NOISE 0
float3 overlay( float3 a, float3 b )
{
@ -284,70 +282,9 @@ float3 BlueNoise3( float2 n, float x )
}
float Noise( float2 n, float x )
{
n += x;
#if 1
return frac( sin( dot( n.xy, float2( 12.9898, 78.233 ) ) ) * 43758.5453 ) * 2.0 - 1.0;
#else
//return BlueNoise( n, 55.0 );
return BlueNoise( n, 1.0 );
//return InterleavedGradientNoise( n ) * 2.0 - 1.0;
#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;
#if DITHER_IN_LINEAR_SPACE
return ( 1.0 / ( a * 4.0 + b * 4.0 - c ) ) * (
#else
return ( 4.0 / ( a * 4.0 + b * 4.0 - c ) ) * (
#endif
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 )
{
#if DITHER_GENERATE_NOISE
float a = Step2( uv, 0.07 );
float b = Step2( uv, 0.11 );
float c = Step2( uv, 0.13 );
return float3( a, b, c );
#else
float3 noise = BlueNoise3( uv, 0.0 );
#if 1
@ -359,19 +296,11 @@ float3 Step3( float2 uv )
#endif
return noise;
#endif
}
// Used for temporal dither.
float3 Step3T( float2 uv )
{
#if DITHER_GENERATE_NOISE
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 );
#else
float3 noise = BlueNoise3( uv, 1.0 );
#if 1
@ -383,7 +312,6 @@ float3 Step3T( float2 uv )
#endif
return noise;
#endif
}
@ -395,86 +323,32 @@ void DitheringPass( inout float4 fragColor, PS_IN fragment )
float3 color = fragColor.rgb;
#if 0
// BOTTOM: Show bands.
if( uv2.y >= 0.975 )
{
// quantized signal
color = float3( uv2.x );
// BOTTOM: Show bands.
color = _float3( uv2.x );
// dithered still
//color = floor( color * Dithering_QuantizationSteps + Step3( uv ) * Dithering_NoiseBoost ) * ( 1.0 / ( Dithering_QuantizationSteps - 1.0 ) );
}
else if( uv2.y >= 0.95 )
{
// quantized signal
color = float3( uv2.x );
color = _float3( uv2.x );
color = floor( color * Dithering_QuantizationSteps ) * ( 1.0 / ( Dithering_QuantizationSteps - 1.0 ) );
}
else if( uv2.y >= 0.925 )
{
// quantized signal dithered temporally
color = float3( uv2.x );
color = _float3( uv2.x );
color = floor( color * Dithering_QuantizationSteps + Step3( uv ) * Dithering_NoiseBoost ) * ( 1.0 / ( Dithering_QuantizationSteps - 1.0 ) );
}
// TOP: Show dither texture.
else if( uv2.y >= 0.9 )
{
// TOP: Show dither texture.
color = Step3( uv ) * ( 0.25 * Dithering_NoiseBoost ) + 0.5;
}
else
#endif
{
#if DITHER_IN_LINEAR_SPACE
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 ) + ( Dithering_NoiseBoost / Dithering_QuantizationSteps ) ) - color;
#else
// Less too expensive.
float luma = PhotoLuma( color );
// Implement this as a texture lookup table.
float amount = Linear1( Srgb1( luma ) + ( Dithering_NoiseBoost / Dithering_QuantizationSteps ) ) - luma;
#endif
#else
// Fast solutions.
#if 1
// Hack 1 (fastest).
// For HDR need saturate() around luma.
float luma = PhotoLuma( color );
float amount = mix(
Linear1( Dithering_NoiseBoost / Dithering_QuantizationSteps ),
Linear1( ( Dithering_NoiseBoost / Dithering_QuantizationSteps ) + 1.0 ) - 1.0,
luma );
#else
// Hack 2 (slower?).
// For HDR need saturate() around color in mix().
float3 amount = mix(
float3( Linear1( Dithering_NoiseBoost / Dithering_QuantizationSteps ) ),
float3( Linear1( ( Dithering_NoiseBoost / Dithering_QuantizationSteps ) + 1.0 ) - 1.0 ),
color );
#endif
#endif
color += Step3T( uv ) * amount;// * Dithering_NoiseBoost;
// 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 * Dithering_QuantizationSteps ) * ( 1.0 / ( Dithering_QuantizationSteps - 1.0 ) );
#else
#if 0
if( uv2.x <= 0.5 )
{
@ -486,8 +360,6 @@ void DitheringPass( inout float4 fragColor, PS_IN fragment )
{
color = floor( 0.5 + color * ( Dithering_QuantizationSteps + Dithering_Wide - 1.0 ) + ( -Dithering_Wide * 0.5 ) + Step3T( uv ) * ( Dithering_Wide ) ) * ( 1.0 / ( Dithering_QuantizationSteps - 1.0 ) );
}
#endif
}
fragColor.rgb = color;

View file

@ -50,8 +50,16 @@ struct PS_OUT
// *INDENT-ON*
#define RESOLUTION_DIVISOR 4.0
#define NUM_COLORS 64
#define RESOLUTION_DIVISOR 4.0
#define NUM_COLORS 64
#define Dithering_QuantizationSteps 8.0 // 8.0 = 2 ^ 3 quantization bits
float3 Quantize( float3 color, float3 period )
{
return floor( color * Dithering_QuantizationSteps ) * ( 1.0 / ( Dithering_QuantizationSteps - 1.0 ) );
//return floor( ( color + period / 2.0 ) / period ) * period;
}
// find nearest palette color using Euclidean distance
@ -74,11 +82,6 @@ float3 LinearSearch( float3 c, float3 pal[NUM_COLORS] )
return pal[idx];
}
float Quantize( float inp, float period )
{
return floor( ( inp + period / 2.0 ) / period ) * period;
}
#define RGB(r, g, b) float3(float(r)/255.0, float(g)/255.0, float(b)/255.0)
void main( PS_IN fragment, out PS_OUT result )
@ -92,23 +95,19 @@ void main( PS_IN fragment, out PS_OUT result )
// 8 * 8 * 8 = 512 colors
// although only 61 colors were available on the screen at the same time but we ignore this for now
const int quantizationSteps = 8;
const int quantizationSteps = Dithering_QuantizationSteps;
float3 quantizationPeriod = _float3( 1.0 / ( quantizationSteps - 1 ) );
// get pixellated base color
float3 color = t_BaseColor.Sample( samp0, uvPixellated * rpWindowCoord.xy ).rgb;
// add Bayer 8x8 dithering
float2 uvDither = fragment.position.xy / ( RESOLUTION_DIVISOR / 1.0 );
float2 uvDither = fragment.position.xy / ( RESOLUTION_DIVISOR / 2.0 );
float dither = DitherArray8x8( uvPixellated ) - 0.5;
color.rgb += float3( dither, dither, dither ) * quantizationPeriod;
// find closest color match from Sega Mega Drive color palette
color = float3(
Quantize( color.r, quantizationPeriod.r ),
Quantize( color.g, quantizationPeriod.g ),
Quantize( color.b, quantizationPeriod.b )
);
color = Quantize( color, quantizationPeriod );
//color = LinearSearch( color.rgb, palette );
//color = float4( BinarySearch( color.rgb, palette ), 1.0 );

View file

@ -51,12 +51,16 @@ struct PS_OUT
#define RESOLUTION_DIVISOR 4.0
#define Dithering_QuantizationSteps 32.0 // 8.0 = 2 ^ 3 quantization bits
float Quantize( float inp, float period )
float3 Quantize( float3 color, float3 period )
{
return floor( ( inp + period / 2.0 ) / period ) * period;
return floor( color * Dithering_QuantizationSteps ) * ( 1.0 / ( Dithering_QuantizationSteps - 1.0 ) );
//return floor( ( color + period / 2.0 ) / period ) * period;
}
void main( PS_IN fragment, out PS_OUT result )
{
float2 uv = ( fragment.texcoord0 );
@ -66,23 +70,56 @@ void main( PS_IN fragment, out PS_OUT result )
// 2^5 = 32
// 32 * 32 * 32 = 32768 colors
const int quantizationSteps = 32;
const int quantizationSteps = Dithering_QuantizationSteps;
float3 quantizationPeriod = _float3( 1.0 / ( quantizationSteps - 1 ) );
// get pixellated base color
float3 color = t_BaseColor.Sample( samp0, uvPixellated * rpWindowCoord.xy ).rgb;
// add Bayer 8x8 dithering
float2 uvDither = fragment.position.xy / ( RESOLUTION_DIVISOR / 1.0 );
float dither = DitherArray8x8( uvDither ) - 0.5;
color.rgb += float3( dither, dither, dither ) * quantizationPeriod;
#if 0
// this looks awesome with 3 bits per color channel
float2 uvDither = fragment.position.xy / ( RESOLUTION_DIVISOR / 2.0 );
#else
// more faithful to the PSX look
float2 uvDither = fragment.position.xy / RESOLUTION_DIVISOR;
#endif
float dither = DitherArray8x8( uvDither ) - 0.5;
#if 0
if( uv.y < 0.05 )
{
color = _float3( uv.x );
}
else if( uv.y < 0.1 )
{
// quantized signal
color = _float3( uv.x );
color = Quantize( color, quantizationPeriod );
}
else if( uv.y < 0.15 )
{
// quantized signal dithered
color = _float3( uv.x );
color = Quantize( color, quantizationPeriod );
color.rgb += float3( dither, dither, dither ) * quantizationPeriod;
}
else if( uv.y < 0.2 )
{
color.rgb = float3( dither, dither, dither ) * quantizationPeriod;
}
else
#endif
{
color.rgb += float3( dither, dither, dither ) * quantizationPeriod;
// PSX color quantization
color = Quantize( color, quantizationPeriod );
}
// PSX color quantization
color = float3(
Quantize( color.r, quantizationPeriod.r ),
Quantize( color.g, quantizationPeriod.g ),
Quantize( color.b, quantizationPeriod.b )
);
//color = t_BaseColor.Sample( samp0, uv ).rgb;

View file

@ -366,6 +366,7 @@ float rand( float2 co )
#define _float3( x ) float3( x, x, x )
#define _float4( x ) float4( x, x, x, x )
#define _int2( x ) int2( x, x )
#define _int3( x ) int3( x, x, x )
#define vec2 float2
#define vec3 float3
#define vec4 float4