The solution to my problem was to pass the uv position and size of each card to a pixelate shader on a viewport that follows the camera. I will build on this for what I specifically need, but maybe this will be a good example for anyone else trying the same thing.
Auto-load Global script with the signal:
signal card_moved(position, size)
Then in process function of each card:
Global.emit_signal("card_moved",position,size*scale)
The viewport container will manage the positions and convert them to uv positions then send them to the shader
extends SubViewportContainer
const position_amount = 32
var vp_resolution : Vector2
var card_positions : Array[Vector2] = []
var card_sizes : Array[Vector2] = []
var card_count = 0 # Number of cards processed
func _ready():
Global.connect("card_moved", _on_card_data)
set_process_priority(-1) # process last
card_positions.resize(position_amount)
card_positions.fill(Vector2.ZERO)
card_sizes.resize(position_amount)
card_sizes.fill(Vector2.ZERO)
card_count = 0
vp_resolution = get_viewport().size
func _on_card_data(flag_position: Vector2, flag_size: Vector2):
var pos_uv = (flag_position + (flag_size*0.5)) / vp_resolution #Convert position to UV
var size_uv = flag_size / vp_resolution # Convert size to UV
card_positions[card_count] = pos_uv
card_sizes[card_count] = size_uv
card_count += 1
if (card_count >= position_amount) :
print("ohh no!")
func clear_data():
card_positions.fill(Vector2.ZERO)
card_sizes.fill(Vector2.ZERO)
card_count = 0
func _process(_delta):
# Now process the data in the ViewportManager
material.set_shader_parameter("card_positions", card_positions)
material.set_shader_parameter("card_sizes", card_sizes)
material.set_shader_parameter("card_count", card_count)
clear_data()
Then the shaders masks those areas and applies the pixelation effect
shader_type canvas_item;
render_mode unshaded;
const int buf_size = 32;
uniform vec2[buf_size] card_positions; // Now in UV space (0-1)
uniform vec2[buf_size] card_sizes; // Now in UV space (0-1)
uniform int card_count = 0;
uniform vec4 test_color = vec4 (0.0);
uniform float border_radius : hint_range(0.0, 0.1, 0.001) = 0.02; // Small rounded corners
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture;
uniform float pixel_size : hint_range (0.5,16.0,0.1) = 0.5;
// Function to draw a rounded rectangle in UV space
float draw_rounded_rectangle(vec2 uv, vec2 size, float radius, float edge) {
edge = max(edge, 1.0e-8); // Prevent zero or negative edge
radius = max(radius, 0.0); // Prevent negative radius
vec2 half_size = size * 0.5; // Half-size for centering
vec2 dist = abs(uv) - (half_size - radius); // Distance from pixel to edges
float corner_dist_squared = dot(max(dist, 0.0), max(dist, 0.0)); // Squared distance to corner
// Return smoothed edge value using squared distance
return clamp((1.0 - (sqrt(corner_dist_squared) - radius) / edge), 0.0, 1.0);
}
void fragment() {
vec2 uv = SCREEN_UV; // UV coordinates
vec2 card_uv = SCREEN_UV;
vec4 color = texture(SCREEN_TEXTURE, uv); // Default background color
float x = FRAGCOORD.x - mod (FRAGCOORD.x, pixel_size);
float y = FRAGCOORD.y - mod (FRAGCOORD.y, pixel_size);
float center = floor(pixel_size / 2.0);
vec4 colorhold = texture(SCREEN_TEXTURE, vec2 (x+center,y+center) * SCREEN_PIXEL_SIZE);
float mask = 0.0; // Accumulate rectangle masks
for (int i = 0; i < card_count; i++){
vec2 card_size = card_sizes[i];
vec2 card_pos = card_positions[i];
float test = draw_rounded_rectangle(card_uv - vec2 (card_pos.x,card_pos.y), card_size, 0.01, 0.00);
color *= (1.0 - test);
color += vec4(colorhold.rgb * (test),test);
}
COLOR = color;
}
Image showing 3 cards sending their position and size and 1 that is not.