Closed RobertBeckebans closed 5 months ago
Neat! The auto dithering seems also very good.
Imagine if you can also apply a color palette to the image.
Backup of experimental local shadertoy shader to dither any image using a palette from lospec.com
//#iChannel0 "file://circlemask_1010p.png"
#iChannel0 "file://WIN_20211016_15_57_04_Pro.jpg"
//#iChannel0 "file://rbdoom-3-bfg-20210219-182155-001.png"
const float PIXEL_FACTOR = 320.; // Lower num - bigger pixels (this will be the screen width)
const float COLOR_FACTOR = 8.; // Higher num - higher colors quality
const mat4 ditherTable = mat4(
-4.0, 0.0, -3.0, 1.0,
2.0, -2.0, 3.0, -1.0,
-3.0, 1.0, -4.0, 0.0,
3.0, -1.0, 2.0, -2.0
);
// https://lospec.com/palette-list/luftrausers7
//#iChannel1 "file://luftrausers7-32x.png"
//#iChannel1 "file://vieilles-cartes-32x.png"
//#iChannel1 "file://smooth-polished-silver-32x.png"
//#iChannel1 "file://justparchment8-8x.png"
//#iChannel1 "file://witching-hour-32x.png"
//#iChannel1 "file://goosebumps-gold-32x.png"
#iChannel1 "file://commodore-8-32x.png"
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Reduce pixels
vec2 size = PIXEL_FACTOR * iResolution.xy/iResolution.x;
vec2 coor = floor( fragCoord/iResolution.xy * size) ;
vec2 uv = coor / size;
// Get source color
vec3 col = texture( iChannel0, uv ).xyz;
// Dither
float dither = ditherTable[int( coor.x ) % 4][int( coor.y ) % 4] * 0.005; // last number is dithering strength
col += vec3( dither );
// Reduce colors
//col = floor(col * COLOR_FACTOR) / COLOR_FACTOR;
// Alias for each color
const float step = 1.0 / 8.0;
vec3 c1 = texture( iChannel1, vec2( 0.0, 0.0 ) ).rgb;
vec3 c2 = texture( iChannel1, vec2( step * 2.0, 0.0) ).rgb;
vec3 c3 = texture( iChannel1, vec2( step * 3.0, 0.0) ).rgb;
vec3 c4 = texture( iChannel1, vec2( step * 4.0, 0.0) ).rgb;
vec3 c5 = texture( iChannel1, vec2( step * 5.0, 0.0) ).rgb;
vec3 c6 = texture( iChannel1, vec2( step * 6.0, 0.0) ).rgb;
vec3 c7 = texture( iChannel1, vec2( step * 7.0, 0.0) ).rgb;
vec3 c8 = texture( iChannel1, vec2( step * 8.0, 0.0) ).rgb;
// Euclidean distance
float d1 = distance(c1, col.rgb);
float d2 = distance(c2, col.rgb);
float d3 = distance(c3, col.rgb);
float d4 = distance(c4, col.rgb);
float d5 = distance(c5, col.rgb);
float d6 = distance(c6, col.rgb);
float d7 = distance(c7, col.rgb);
float d8 = distance(c8, col.rgb);
// Best fit search
vec4 rgb_d = vec4(c1, d1);
rgb_d = rgb_d.a < d2 ? rgb_d : vec4(c2, d2);
rgb_d = rgb_d.a < d3 ? rgb_d : vec4(c3, d3);
rgb_d = rgb_d.a < d4 ? rgb_d : vec4(c4, d4);
rgb_d = rgb_d.a < d5 ? rgb_d : vec4(c5, d5);
rgb_d = rgb_d.a < d6 ? rgb_d : vec4(c6, d6);
rgb_d = rgb_d.a < d7 ? rgb_d : vec4(c7, d7);
rgb_d = rgb_d.a < d8 ? rgb_d : vec4(c8, d8);
col = rgb_d.rgb;
// Output to screen
fragColor = vec4(col, 1.0);
}
Output:
Backup of some interesting palettes:
This can be done in a more efficient way: see https://www.shadertoy.com/view/MtjGRd
#define DITHER
#define AUTO_MODE
#define DOWN_SCALE 2.0
#define MAX_STEPS 196
#define MIN_DIST 0.002
#define NORMAL_SMOOTHNESS 0.1
#define PI 3.14159265359
#define PALETTE_SIZE 16
#define SUB_PALETTE_SIZE 8
#define RGB(r,g,b) (vec3(r,g,b) / 255.0)
vec3 palette[PALETTE_SIZE];
vec3 subPalette[SUB_PALETTE_SIZE];
//Initalizes the color palette.
void InitPalette()
{
//16-Color C64 color palette.
palette = vec3[](
RGB( 0, 0, 0),
RGB(255,255,255),
RGB(152, 75, 67),
RGB(121,193,200),
RGB(155, 81,165),
RGB(104,174, 92),
RGB( 62, 49,162),
RGB(201,214,132),
RGB(155,103, 57),
RGB(106, 84, 0),
RGB(195,123,117),
RGB( 85, 85, 85),
RGB(138,138,138),
RGB(163,229,153),
RGB(138,123,206),
RGB(173,173,173)
);
//8-Color metalic-like sub palette.
subPalette = vec3[](
palette[ 6],
palette[11],
palette[ 4],
palette[14],
palette[ 5],
palette[ 3],
palette[13],
palette[ 1]
);
}
//Blends the nearest two palette colors with dithering.
vec3 GetDitheredPalette(float x,vec2 pixel)
{
float idx = clamp(x,0.0,1.0)*float(SUB_PALETTE_SIZE-1);
vec3 c1 = vec3(0);
vec3 c2 = vec3(0);
c1 = subPalette[int(idx)];
c2 = subPalette[int(idx) + 1];
#ifdef DITHER
float dith = texture(iChannel0, pixel / iChannelResolution[0].xy).r;
float mixAmt = float(fract(idx) > dith);
#else
float mixAmt = fract(idx);
#endif
return mix(c1,c2,mixAmt);
}
//Returns a 2D rotation matrix for the given angle.
mat2 Rotate(float angle)
{
return mat2(cos(angle), sin(angle), -sin(angle), cos(angle));
}
//Distance field functions & operations by iq. (https://iquilezles.org/articles/distfunctions)
float opU( float d1, float d2 )
{
return min(d1,d2);
}
float opS( float d1, float d2 )
{
return max(-d1,d2);
}
float opI( float d1, float d2 )
{
return max(d1,d2);
}
vec3 opRep( vec3 p, vec3 c )
{
vec3 q = mod(p,c)-0.5*c;
return q;
}
float sdSphere( vec3 p, float s )
{
return length(p)-s;
}
float sdBox( vec3 p, vec3 b )
{
vec3 d = abs(p) - b;
return min(max(d.x,max(d.y,d.z)),0.0) +
length(max(d,0.0));
}
float sdCylinder( vec3 p, vec3 c )
{
return length(p.xz-c.xy)-c.z;
}
//Scene definition/distance function.
float Scene(vec3 pos)
{
float map = -sdSphere(pos, 24.0);
vec3 rep = opRep(pos - 2.0, vec3(4.0));
map = opU(map, opI(sdBox(pos, vec3(5.5)), sdSphere(rep, 1.0)));
vec3 gSize = vec3(0, 0, 0.25);
float grid = opU(opU(sdCylinder(rep.xyz, gSize), sdCylinder(rep.xzy, gSize)), sdCylinder(rep.zxy, gSize));
grid = opI(sdBox(pos,vec3(4.5)),grid);
map = opU(map, grid);
return map;
}
//Returns the normal of the surface at the given position.
vec3 Normal(vec3 pos)
{
vec3 offset = vec3(NORMAL_SMOOTHNESS, 0, 0);
vec3 normal = vec3
(
Scene(pos - offset.xyz) - Scene(pos + offset.xyz),
Scene(pos - offset.zxy) - Scene(pos + offset.zxy),
Scene(pos - offset.yzx) - Scene(pos + offset.yzx)
);
return normalize(normal);
}
//Marches a ray defined by the origin and direction and returns the hit position.
vec3 RayMarch(vec3 origin,vec3 direction)
{
float hitDist = 0.0;
for(int i = 0;i < MAX_STEPS;i++)
{
float sceneDist = Scene(origin + direction * hitDist);
hitDist += sceneDist;
if(sceneDist < MIN_DIST)
{
break;
}
}
return origin + direction * hitDist;
}
//Scene shading.
vec3 Shade(vec3 position, vec3 normal, vec3 rayOrigin,vec3 rayDirection,vec2 pixel)
{
vec3 color = vec3(0);
float ang = iTime * 2.0;
vec3 lightPos = vec3(cos(ang), cos(ang*2.0), sin(ang)) * 2.0;
//Normal shading
float shade = 0.4 * max(0.0, dot(normal, normalize(-lightPos)));
//Specular highlight
shade += 0.6 * max(0.0, dot(-reflect(normalize(position - lightPos), normal), rayDirection));
//Linear falloff
shade *= (16.0-distance(position, lightPos))/16.0,
//Apply palette
color = GetDitheredPalette(shade, pixel);
//color = mix(color, vec3(0.1), step(22.0, length(position)));
return color;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
InitPalette();
vec2 aspect = iResolution.xy / iResolution.y;
fragCoord = floor(fragCoord / DOWN_SCALE) * DOWN_SCALE;
vec2 uv = fragCoord.xy / iResolution.y;
vec2 mouse = iMouse.xy / iResolution.xy - 0.5;
vec2 camAngle = vec2(0);
#ifdef AUTO_MODE
camAngle.x = PI * (-1.0 / 8.0) * sin(iTime * 0.5);
camAngle.y = -iTime;
#else
camAngle.x = PI * mouse.y + PI / 2.0;
camAngle.x += PI / 3.0;
camAngle.y = 2.0 * PI * -mouse.x;
camAngle.y += PI;
#endif
vec3 rayOrigin = vec3(0 , 0, -16.0);
vec3 rayDirection = normalize(vec3(uv - aspect / 2.0, 1.0));
mat2 rotateX = Rotate(camAngle.x);
mat2 rotateY = Rotate(camAngle.y);
//Transform ray origin and direction
rayOrigin.yz *= rotateX;
rayOrigin.xz *= rotateY;
rayDirection.yz *= rotateX;
rayDirection.xz *= rotateY;
vec3 scenePosition = RayMarch(rayOrigin, rayDirection);
vec3 outColor = Shade(scenePosition,Normal(scenePosition), rayOrigin, rayDirection, fragCoord / DOWN_SCALE);
//Palette preview
if(uv.x < 0.05)
{
outColor = GetDitheredPalette(uv.y, fragCoord / DOWN_SCALE);
}
fragColor = vec4(outColor, 1.0);
}```
Very interesting R2 sequence dither algo suited for 1-bit aesthetics for game jams: https://www.shadertoy.com/view/WdjGWy
// Dithering using the R-mask
// http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/
// Uncomment the #define to get color
#define PER_CHANNEL
// Triangle Wave
float T(float z) {
return z >= 0.5 ? 2.-2.*z : 2.*z;
}
// R dither mask
float intensity(ivec2 pixel) {
const float a1 = 0.75487766624669276;
const float a2 = 0.569840290998;
return fract(a1 * float(pixel.x) + a2 * float(pixel.y));
}
float dither(float gray, int ng) {
// Calculated noised gray value
float noised = (2./float(ng)) * T(intensity(ivec2(gl_FragCoord.xy))) + gray - (1./float(ng));
// Clamp to the number of gray levels we want
return clamp(floor(float(ng) * noised) / (float(ng)-1.), 0.f, 1.f);
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
const int ng = 2; // Number of gray levels to use
vec2 uv = fragCoord/iResolution.xy;
vec3 tsample = pow(texture(iChannel0, uv).rgb, vec3(2.2));
#ifdef PER_CHANNEL
vec3 col = vec3(dither(tsample.r, ng),
dither(tsample.g, ng),
dither(tsample.b, ng));
#else
vec3 col = vec3(dither(dot(tsample, vec3(0.3, 0.59, 0.11)), ng));
#endif
// Output to screen, gamma corrected
fragColor = vec4(vec3(pow(col, vec3(1.0/2.2))),1.0);
}
2 awesome C64 shaders: C64 screen with NTSC filter - https://www.shadertoy.com/view/3tVBWR Palettization + OrderedDithering (very close to my previous shader) - https://www.shadertoy.com/view/Xdt3Dr
Another good C64 shader:
#define RGB(r, g, b) vec3(float(r)/255., float(g)/255., float(b)/255.)
// C64 palette: http://unusedino.de/ec64/technical/misc/vic656x/colors/
#define NUM_COLORS 16
vec3 palette[NUM_COLORS];
// pre GLES3 GPUs don't support array constructors, so need to initialize array explicitly
void InitPalette()
{
palette[0] = RGB(0, 0, 0);
palette[1] = RGB(255, 255, 255);
palette[2] = RGB(116, 67, 53);
palette[3] = RGB(124, 172, 186);
palette[4] = RGB(123, 72, 144);
palette[5] = RGB(100, 151, 79);
palette[6] = RGB(64, 50, 133);
palette[7] = RGB(191, 205, 122);
palette[8] = RGB(123, 91, 47);
palette[9] = RGB(79, 69, 0);
palette[10] = RGB(163, 114, 101);
palette[11] = RGB(80, 80, 80);
palette[12] = RGB(120, 120, 120);
palette[13] = RGB(164, 215, 142);
palette[14] = RGB(120, 106, 189);
palette[15] = RGB(159, 159, 150);
}
// find nearest palette color using Euclidean distance
vec4 EuclidDist(vec3 c, vec3[NUM_COLORS] pal)
{
int idx = 0;
float nd = distance(c, pal[0]);
for(int i = 1; i < NUM_COLORS; i++)
{
float d = distance(c, pal[i]);
if(d < nd)
{
nd = d;
idx = i;
}
}
/*
// older GPUs/drivers require constant array indexing, so can't use idx directly
if(idx == 0) return vec4(pal[0], 1.);
if(idx == 1) return vec4(pal[1], 1.);
if(idx == 2) return vec4(pal[2], 1.);
if(idx == 3) return vec4(pal[3], 1.);
if(idx == 4) return vec4(pal[4], 1.);
if(idx == 5) return vec4(pal[5], 1.);
if(idx == 6) return vec4(pal[6], 1.);
if(idx == 7) return vec4(pal[7], 1.);
if(idx == 8) return vec4(pal[8], 1.);
if(idx == 9) return vec4(pal[9], 1.);
if(idx == 10) return vec4(pal[10], 1.);
if(idx == 11) return vec4(pal[11], 1.);
if(idx == 12) return vec4(pal[12], 1.);
if(idx == 13) return vec4(pal[13], 1.);
if(idx == 14) return vec4(pal[14], 1.);
return vec4(pal[15], 1.);
*/
// sleek but not guaranteed to work on older GPUs!
return vec4(pal[idx], 1.);
}
void mainImage(out vec4 o, in vec2 p)
{
vec2 uv = vec2(floor(p.x * .25) * 4., floor(p.y * .5) * 2.) / iResolution.xy;
InitPalette();
o = EuclidDist(texture(iChannel0, uv).rgb, palette);
}
The color palette of the CPC 6128, my first computer: https://www.cpcwiki.eu/index.php/CPC_Palette
References:
https://github.com/RobertBeckebans/KinoEight/blob/master/Packages/jp.keijiro.kino.post-processing.eight/Resources/EightColor.shader
https://www.shadertoy.com/view/tsKGDm