CanvasGroup pixelation shader

Godot Version

4.2

Question

I’m trying to create a shader for pixelating an entire CanvasGroup. I have a few requirements:

  • The children of the CanvasGroup should all be pixelated to the same grid.
  • If I create multiple CanvasGroups with this shader, they too should use the same grid. Meaning the pixels need to be aligned to the x-y-axis.

Here is a picture of what I’m trying to achieve:
image
The individual grasses are aligned on the same grid, and with the x-y-axis.

Here is my attempt. It stops working if I zoom the camera in or out, and doesn’t work well across the x or y axis (as you can see in the picture).

shader_type canvas_item;
render_mode world_vertex_coords;

uniform int pixel_size = 8;

uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;

varying vec2 world_pos;

void vertex() {
	world_pos = VERTEX;
}

void fragment() {
	vec2 texture_size = vec2(textureSize(screen_texture, 0));
	vec2 screen_position = world_pos - SCREEN_UV * texture_size;
	vec2 pixel = vec2(ivec2(world_pos / float(pixel_size)));
	vec2 relative_pixel = pixel - screen_position / float(pixel_size);
	vec2 pixel_uv = relative_pixel / (texture_size / float(pixel_size)) ;
	
	vec4 c = textureLod(screen_texture, pixel_uv, 0);
	
	if (c.a > 0.0001) {
		c.rgb /= c.a;
	}
	COLOR *= c;
}

you will need to actually “rescale” the screen uv with a variable so it stays the same when zooming in and out

this video explains how the scale with zoom works for shader

1 Like

Thank you so much for pointing me in the right direction! That fixed the zoom problem… almost. There is still a small change in color when zooming that I don’t know how to get rid of. See this video.

Flooring the pixel value instead of converting it to an int fixes the issue around the x and y axis.

There is one more small issue: the first pixel lands on the x-y-axis:
image
I would prefer for it to be next to the axis.

The updated shader looks like this:

shader_type canvas_item;
render_mode world_vertex_coords;

uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;

uniform int pixel_size = 8;
uniform vec2 zoom = vec2(1.0);

varying vec2 world_pos;

void vertex() {
	world_pos = VERTEX;
}

void fragment() {
	vec2 screen_size = vec2(textureSize(screen_texture, 0));
	vec2 screen_position = zoom * world_pos - SCREEN_UV * screen_size;
	vec2 pixel = zoom * floor(world_pos / float(pixel_size) + 0.5);
	vec2 relative_pixel = pixel - screen_position / float(pixel_size);
	vec2 pixel_uv = (relative_pixel) * float(pixel_size) / screen_size;
	
	vec4 c = textureLod(screen_texture, pixel_uv, 0);
	
	if (c.a > 0.0001) {
		c.rgb /= c.a;
	}
	COLOR *= c;
}

And this is the script attached to the node:

@tool
extends CanvasGroup

func _process(delta):
	material.set_shader_parameter("zoom", get_viewport().global_canvas_transform.get_scale());

Also worth noting is that if the stretch mode is set to canvas_item, we cannot use get_viewport().global_canvas_transform.get_scale() to get the zoom, as it is always 1.0. Instead we must do:

var zoom_2d: Vector2 = Vector2(get_viewport().size) / get_viewport_rect().size
var zoom: float = min(zoom_2d.x, zoom_2d.y)

my script that controlling y_zoom looks like this:

func _process(delta):
	_zoom_changed()

func _zoom_changed():
	var scale_y=get_viewport_transform().get_scale().y
	material.set_shader_parameter("y_zoom",scale_y)

godot 4.1.3

have you tried other filters option? like :
image

Thanks, that seems to work regardless of what stretch mode we are using!

Yes, I makes no difference which filter option I’m using. I still get the flickering effect when zooming.

Actually, using filter_linear_mipmap seems to make a small difference, but it’s still not perfect. Much closer though!

you might want to merge your code with this sub pixel perfect shader, as it should make your pixelated not flickering when moving/scaled.

I tried using the sub pixel perfect shader, but it did not make a noticeable difference. Using the filter option filter_linear_mipmap did make it near perfect however.

did your sprite in which you add shader material on has been scaled?
or it’s perfectly 1,1 scale in size?
i have a hunch that you might need to send this newly scale parameter of the current sprite into the shader so it doesnt have that flicker

never mind, any sprite/textures can fit into that canvas group, so your sprite will always might not be the same scale, and the shader should never know the scale of each of the item inside it.

Yeah, that shouldn’t matter. And changing the scale of the CanvasGroup doesn’t make a difference either.
It is good enough now however, really it’s barely noticeable. Thank you so much for your help!

The only thing that is annoying me now is that I can’t get the first pixel to land next to the x-y-axis, instead of on it…