// https://github.com/Rudranil-Sarkar/Ordered-Dithering-Shader-GLSL/blob/master/Dither8x8.frag // http://alex-charlton.com/posts/Dithering_on_the_GPU/ #import bevy_pbr::mesh_view_bindings @group(1) @binding(0) var texture: texture_2d; @group(1) @binding(1) var our_sampler: sampler; @group(1) @binding(2) var noise: texture_2d; @group(1) @binding(3) var noise_sampler: sampler; @group(1) @binding(4) var palette: array, 16>; let dither_table = array( 0, 48, 12, 60, 3, 51, 15, 63, 32, 16, 44, 28, 35, 19, 47, 31, 8, 56, 4, 52, 11, 59, 7, 55, 40, 24, 36, 20, 43, 27, 39, 23, 2, 50, 14, 62, 1, 49, 13, 61, 34, 18, 46, 30, 33, 17, 45, 29, 10, 58, 6, 54, 9, 57, 5, 53, 42, 26, 38, 22, 41, 25, 37, 21 ); fn hsl2rgb(c: vec3) -> vec3 { let rgb = clamp( abs( ((c.x * 6.0 + vec3(0.0,4.0,2.0)) % 6.0) - 3.0 ) - 1.0, vec3(0.0), vec3(1.0) ); return c.z + c.y * (rgb - .5) * (1. - abs(2. * c.z - 1.)); } fn rgb2hsl(c: vec3) -> vec3 { var h = 0.; var s = 0.; var l = 0.; var r = c.r; var g = c.g; var b = c.b; var cMin = min(r, min(g, b)); var cMax = max(r, max(g, b)); l = (cMax + cMin) / 2.; if (cMax > cMin) { var cDelta = cMax - cMin; if (l < 0.0) { s = cDelta / (cMax+cMin); } else { s = cDelta / (2. - (cMax + cMin)); } if(r == cMax) { h = (g - b) / cDelta; } else if (g == cMax) { h = 2. + (b - r) / cDelta; } else { h = 4. + (r - g) / cDelta; } if(h < 0.) { h += 6.; } h = h / 6.; } return vec3(h, s, l); } fn hueDistance(h1: f32, h2: f32) -> f32 { let diff = abs(h1 - h2); return min(abs(1. - diff), diff); } let lightnessSteps: f32 = 4.; // SOURCE: http://alex-charlton.com/posts/Dithering_on_the_GPU/ fn lightnessStep(l: f32) -> f32 { return floor((.5 + l * lightnessSteps)) / lightnessSteps; } let SaturationSteps: f32 = 4.; fn SaturationStep(s: f32) -> f32 { /* Quantize the saturation to one of SaturationSteps values */ return floor((.5 + s * SaturationSteps)) / SaturationSteps; } fn Closest_color(hue: f32) -> array, 2> { var closest = vec3(-2., 0., 0.); var secondClosest = vec3(-2., 0., 0.); var temp: vec3; var p: array, 16> = palette; for(var i: i32 = 0; i < 16; i++) { temp = p[i]; let tempDistance = hueDistance(temp.x, hue); if(tempDistance < hueDistance(closest.x, hue)) { secondClosest = closest; closest = temp; } else if(tempDistance < hueDistance(secondClosest.x, hue)){ secondClosest = temp; } } var result: array, 2>; result[0] = closest; result[1] = secondClosest; return result; } fn dither(pos: vec2, color: vec3) -> vec3 { let x = floor(pos.x % 8.); let y = floor(pos.y % 8.); let index = i32(x + y * 8.); // This variable can be adjusted in range of -1 to 1 for more desirable result let bias = 0.0; var p: array = dither_table; let limit = (f32(p[index]) + 1.) / 64. + bias; let Colors: array, 2> = Closest_color(color.x); let hueDiff = hueDistance(color.x, Colors[0].x) / hueDistance(Colors[1].x, Colors[0].x); let l1 = lightnessStep(max((color.z - .125), 0.)); let l2 = lightnessStep(min((color.z + .124), 1.)); let lightnessDiff = (color.z - l1) / (l2 - l1); var resultColor: vec3; if (hueDiff < limit) { resultColor = Colors[0]; } else { resultColor = Colors[1]; } if (lightnessDiff < limit) { resultColor.z = l1; } else { resultColor.z = l2; } let s1 = SaturationStep(max((color.y - .125), 0.)); let s2 = SaturationStep(min((color.y + .124), 1.)); let SaturationDiff = (color.y - s1) / (s2 - s1); if (SaturationDiff < limit) { resultColor.y = s1; } else { resultColor.y = s2; } return hsl2rgb(resultColor); } @fragment fn fragment( @builtin(position) position: vec4, #import bevy_sprite::mesh2d_vertex_output ) -> @location(0) vec4 { let uv = position.xy / vec2(view.width, view.height); // let uv = floor(uv * 400.0) / 400.0; let col = textureSample(texture, our_sampler, uv); return vec4(dither(position.xy / 2.0, rgb2hsl(col.rgb)), col.a); // return round(col * 50.0) / 50.0; // return col; }