Mobile-Vulkan De-Banding Post Process
Since Mobile Vulkan as Introduced to godot, there’s a frustrating issue with the Mobile Renderer: for some reason, the team decided to use the RGB10A2 format for rendering. While it does support HDR and offers good performance, it produces noticeably poor visual quality in darker areas — somehow even worse than the Compatibility renderer. To work around this, I created a shader aimed at minimizing the negative visual impact.
A Blue Noise Texture is included for use with this shader
Shader code
shader_type canvas_item;
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, repeat_disable, filter_nearest;
uniform sampler2D BlueNoise : source_color, repeat_enable, filter_nearest;
uniform float Radius : hint_range(1.0, 128.0, 1.0) = 128.0;
uniform float Threshold : hint_range(0.0, 0.1, 0.0001) = 0.02;
uniform bool StaticNoise = false;
const float PI2 = PI * 2.0;
// Use normal distribution (Box-Muller) for offset generation
vec2 random2(vec2 rand) {
float angle = rand.x * PI2;
float radius = sqrt(-2.0 * log(max(rand.x, 1e-6)));
return vec2(cos(angle), sin(angle)) * radius;
}
// Blue noise lookup with optional temporal jitter + rotation
vec4 random4(vec2 pos, vec2 noise_inv_size) {
vec2 p = pos;
if (!StaticNoise) {
vec2 jitter = vec2(sin(TIME * 13.1), cos(TIME * 17.7)) * 0.1;
float angle = TIME * 0.75;
mat2 rot = mat2(
vec2(cos(angle), sin(angle)),
vec2(-sin(angle), cos(angle))
);
p = rot * pos + jitter;
}
return texture(BlueNoise, fract(p * noise_inv_size));
}
void average(vec2 frag_coord, inout vec4 tmp, float radius, vec2 rand, vec2 inv_screen_size) {
vec2 offset = random2(rand) * radius;
vec4 avg = texture(SCREEN_TEXTURE, (frag_coord + offset * vec2(-1.0, -1.0)) * inv_screen_size)
+ texture(SCREEN_TEXTURE, (frag_coord + offset * vec2(-1.0, +1.0)) * inv_screen_size)
+ texture(SCREEN_TEXTURE, (frag_coord + offset * vec2(+1.0, -1.0)) * inv_screen_size)
+ texture(SCREEN_TEXTURE, (frag_coord + offset * vec2(+1.0, +1.0)) * inv_screen_size);
avg *= 0.25;
vec4 mask = step(vec4(Threshold), abs(tmp - avg));
tmp = mix(avg, tmp, mask);
}
vec4 deband(vec2 frag_coord, vec4 src, vec4 rand, vec2 inv_screen_size, float base_radius) {
vec4 tmp = src;
float jittered_radius = base_radius * (0.95 + 0.1 * rand.w);
average(frag_coord, tmp, 8.0, rand.yz, inv_screen_size);
average(frag_coord, tmp, jittered_radius, rand.zw, inv_screen_size);
return tmp;
}
void fragment() {
vec2 frag = FRAGCOORD.xy;
vec2 screen_size = vec2(textureSize(SCREEN_TEXTURE, 0));
vec2 inv_screen_size = 1.0 / screen_size;
vec2 noise_inv_size = 1.0 / max(vec2(textureSize(BlueNoise, 0)), vec2(1.0));
vec4 src = texture(SCREEN_TEXTURE, frag * inv_screen_size);
vec4 rand = random4(frag, noise_inv_size);
vec4 base_c = deband(frag, src, rand, inv_screen_size, Radius);
vec4 noise_c = rand * Threshold * (src - base_c);
vec4 color = base_c + noise_c;
COLOR = color;
}




