Using a Shader while Upsampling Viewport to Window Size

Godot Version

v4.2.1.stable.official [b09f793f5]

Brief

I want to manually control (e.g. via a shader) how the viewport configured via the project settings is scaled to window/screen size.

Question

I am developing a pixel art game using pixel-perfect graphics. This is implemented in the following way: In Project Settings → Display → Window, the viewport size is set to the base pixel resolution (in my case: 240x160) on which I do all pixel perfect rendering. This is then scaled up to window size / full screen size via the Stretch settings mode=viewport, aspect=keep, mode=fractional.

It is widely understood that scaling the viewport up to window/screen size causes “pixel wobble” when the window/screen size is not an exact multiple of the viewport size as viewport pixels cannot perfectly be mapped onto the target resolution:

px

One strategy to mitigate this is called a “Sharp Bilinear Shader” which keeps the pixels apparently square by blurring pixel edges where needed. With this, a square pixel appearance is preserved with minimal blurring. Examples and explanations are here and here.

However, Godot does not seem to provide the functionality to select a filter for the stretching or provide a custom shader for this.

My question is: How can I use a custom filter/shader for upsampling my game from base resolution to window size? Is there some way I can circumvent the Godot default functionality via the settings menu and code the upsampling myself? How can I get control over this process?

you need to use a screen space shader.

You could also modify the projection matrix with a shader as well. or apply per canvas item material to modify how it is drawn.

you can also just zoom your camera to an even integer value like this.

example
extends Camera2D


const height := 16
const width  := 16
const ratio :float= 16.0/16.0
var target : Vector2 = Vector2(1,1)
func _ready():
	print(name," connecting")
	get_viewport().size_changed.connect(size_changed)
	get_window().min_size = Vector2i(height,width)

func size_changed():
	print(get_window().size)
	var size : Vector2i = get_window().size
	if size.y % 2:
		size.y += 1
	size.x = size.y * ratio
	var s = Vector2(size) / Vector2(width,height)
	target = s

func _process(delta):
	if not zoom.is_equal_approx(target):
		zoom = zoom.lerp(target,0.2)
	else:
		zoom = target
	

@pennyloafers Thanks a lot for your response. I’ve realized that I still lack the knowledge to immediately apply your suggestions.

I looked into screen-reading shaders and post processing. Question: How can I apply such a shader not on my viewport resolution but on the actual stretched screen resolution? I used the setup described in the “Custom post-processing” section, adding an additional CanvasLayer with a ColorRect having the shader attached. But the fragments for which the shader is run are now the pixels of my viewport, not the pixels of the stretched window/screen.

For a technique like the one described in my initial post, I would need to modify each screen pixel, not the pixels of my viewport.

I think I understand what you are saying.

I found this
https://www.reddit.com/r/godot/comments/16unlug/understanding_image_scaling_with_viewport/

I was looking around more and you modify the screen pixels in the shader. Take a look at this.